Skip to content

feat(mcp): heatmap chart type plugin - #43570

Open
gkneighb wants to merge 3 commits into
apache:masterfrom
gkneighb:feat/mcp-heatmap-plugin
Open

gkneighb wants to merge 3 commits into
apache:masterfrom
gkneighb:feat/mcp-heatmap-plugin

Conversation

@gkneighb

Copy link
Copy Markdown
Contributor

SUMMARY

Adds a generate_chart MCP plugin for the heatmap chart type (viz_type: heatmap_v2), so the MCP generate_chart tool can produce heatmaps. Heatmap is a shipped Superset viz type that had no MCP plugin; this closes that gap.

The plugin mirrors the frontend Heatmap buildQuery contract: an x_axis column and a single groupby column form the two axes, one metric colours each cell, and normalize_across selects the server-side rank-normalization range (the whole heatmap, per-x column, or per-y row).

Follows the established plugin pattern (pie/funnel/gauge/treemap/waterfall) — a config schema, a map_*_config mapper, a plugin class, and registration:

  • HeatmapChartConfig — x_axis, y_axis, and metric required; optional normalize_across (default heatmap), row_limit, filters. Added to the ChartConfig discriminated union and the get_chart_type_schema adapters. The Y axis accepts the native groupby alias and is emitted as a single-select groupby (not a list), matching the frontend control (multi: false). x_axis/y_axis may not be saved_metric/sql_expression (dimensions, not metrics).
  • map_heatmap_config — maps the config to form_data; saved metrics pass through as a bare name string, ad-hoc metrics as SIMPLE/SQL adhoc objects. The server applies the rank-normalization post-processing operator (driven by normalize_across).
  • HeatmapChartPlugin — registered in the plugin registry; heatmap_v2 was already present in the recommendation category map.

The field set is intentionally minimal (core query contract + normalization). Cosmetic controls (linear color scheme, legend/value display, sort axes, scale intervals) are left for a follow-up.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend/MCP only, no UI change.

TESTING INSTRUCTIONS

pytest tests/unit_tests/mcp_service/chart/test_heatmap_chart.py — 18 unit tests cover schema validation (x/y/metric required, native groupby aliasing for the Y axis, x_axis/y_axis-not-a-metric, normalize_across enum, extra-field rejection), ChartConfig union dispatch, form_data mapping (viz_type/x_axis/groupby-as-single/metric/normalize/filters/saved-metric), and registry integration (registration, resolve_viz_type, display_name, pre_validate).

To exercise end-to-end: call the generate_chart MCP tool with
{"chart_type": "heatmap_v2", "x_axis": {"name": "day_of_week"}, "y_axis": {"name": "hour"}, "metric": {"name": "trips", "aggregate": "COUNT"}}.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

@codecov

codecov Bot commented Aug 27, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.05495% with 50 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.12%. Comparing base (342a8f1) to head (7b09f5c).
⚠️ Report is 180 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/plugins/heatmap.py 34.54% 36 Missing ⚠️
superset/mcp_service/chart/chart_utils.py 28.57% 5 Missing ⚠️
superset/mcp_service/chart/schemas.py 70.58% 5 Missing ⚠️
superset/common/form_data_query_context.py 66.66% 2 Missing and 1 partial ⚠️
superset/mcp_service/chart/chart_helpers.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #43570       +/-   ##
===========================================
- Coverage   81.97%   66.12%   -15.86%     
===========================================
  Files        2988     2989        +1     
  Lines      184142   184229       +87     
  Branches    42565    42581       +16     
===========================================
- Hits       150952   121816    -29136     
- Misses      30427    59764    +29337     
+ Partials     2763     2649      -114     
Flag Coverage Δ
hive 36.38% <39.56%> (+<0.01%) ⬆️
mysql 55.20% <45.05%> (-0.01%) ⬇️
postgres 55.21% <45.05%> (?)
presto 38.26% <39.56%> (+<0.01%) ⬆️
python 55.52% <45.05%> (-30.48%) ⬇️
sqlite 54.93% <45.05%> (-0.01%) ⬇️
unit ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from 6f2097f to d255e55 Compare August 27, 2026 10:34
@gkneighb
gkneighb marked this pull request as ready for review August 27, 2026 11:21
@dosubot dosubot Bot added the viz:charts:heatmap Related to the Heatmap chart label Aug 27, 2026
@bito-code-review

bito-code-review Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #251ea5

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/mcp_service/chart/schemas.py - 1
    • Inconsistent chart_type docs · Line 2147-2150
      The diff updates the `ChartConfig` union description to advertise `heatmap_v2`, but `get_chart_type_schema.py`'s own docstring (lines 252-254, `Valid chart_type values: xy, table, pie, pivot_table, mixed_timeseries, handlebars, big_number, histogram, box_plot, waterfall.`) still omits it. MCP clients reading that docstring will think `heatmap_v2` is not a valid input even though the tool's adapter dict already handles it. Add `heatmap_v2` to that list to keep the discovery surface consistent with the schema union.
Review Details
  • Files reviewed - 6 · Commit Range: d255e55..d255e55
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@netlify

netlify Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 7b09f5c
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6abf265ea5079f00084487e0
😎 Deploy Preview https://deploy-preview-43570--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@aminghadersohi aminghadersohi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — feat(mcp): heatmap chart type plugin

Thanks for closing out the MCP chart-plugin family, and for the thorough test suite. One functional issue in the query path that I confirmed by running the code, plus a secondary observation and a test-coverage note. No blocking asks from me — flagging for your call as the external-contributor gate still governs merge.

Method note: findings below are marked RAN (executed against this PR's head 55caf415 with superset-core on PYTHONPATH) or INSPECTED (read-only).


1. 🔴 x_axis is silently dropped from the query — heatmap collapses to one dimension (RAN)

map_heatmap_config (chart_utils.py) emits the X axis under the x_axis key and the Y axis under groupby. But the MCP query builder only folds x_axis into the query columns for time-series viz types:

# chart_helpers.py — build_query_dicts_from_form_data
is_timeseries = (
    viz_type.startswith("echarts_timeseries") or viz_type == "mixed_timeseries"
)
x_axis_col = None
if is_timeseries:                       # heatmap_v2 is neither → skipped
    x_axis_col = extract_x_axis_col(form_data)
    if x_axis_col and x_axis_col not in groupby:
        groupby = [x_axis_col] + groupby

heatmap_v2 is not a time-series type, so x_axis is never consumed. resolve_groupby returns only the groupby (Y) column, and the built query becomes columns=["hour"] — i.e. SELECT hour, COUNT(trips) … GROUP BY hour. The entire X axis (day_of_week) is gone, and the aggregation grain is wrong.

Executed repro at this PR's head:

form_data keys: ['groupby', 'metric', 'normalize_across', 'row_limit', 'viz_type', 'x_axis']
x_axis in form_data: day_of_week      groupby in form_data: hour
viz_type: heatmap_v2 | is_timeseries: False
resolved groupby (GROUP BY cols): ['hour']
FINAL groupby after x_axis fold: ['hour']
>>> day_of_week (x_axis) present in GROUP BY? False

The frontend Heatmap/buildQuery.ts — which the docstrings say this mapping matches — sets both axes as query columns:

const columns = [
  ...ensureIsArray(getXAxisColumn(formData)),  // x_axis
  ...ensureIsArray(groupby),                    // y
];

This is the same class of silent drop that hit the siblings (sankey's missing GROUP BY grain, bubble's dropped metrics): the mapper emits a frontend-shaped key that the direct query-context path never reads. It manifests in get_chart_data, get_chart_preview, and get_chart_sql, all of which build the query context from this form_data rather than running the frontend buildQuery. Suggested fix: fold x_axis into the query columns for heatmap_v2 too (or, generally, whenever form_data carries an x_axis and the viz isn't handled elsewhere), so both dimensions reach GROUP BY.

2. normalize_across maps to nothing on this path (INSPECTED)

The schema documents normalize_across as driving "the server-side rank normalization", and map_heatmap_config emits it into form_data. But the frontend implements that normalization as a rankOperator post-processing step inside buildQuery, and the MCP query path adds no post_processing — normalize_across is written but never consumed (grep finds it only in the mapper/docstring, never in the query builder). Same root cause as #1: a buildQuery-only transform is lost. Lower severity — the cells still render, just un-normalized relative to the frontend — but the "server-side rank normalization" claim isn't true for MCP-executed queries.

3. Tests pin a shape the query path doesn't honor (RAN — 18/18 pass)

The suite is green even with #1 present, because it stops at the mapper output. test_basic_heatmap_form_data asserts form_data["x_axis"] == "day_of_week" and treats that as the contract, but no test builds a query context / SQL from the form_data to confirm x_axis actually reaches GROUP BY. That's the sankey pattern — a passing test cementing a shape the downstream never reads. A query-context-level test (assert both day_of_week and hour appear in the resulting columns) would have caught #1 and would guard the fix.


What's genuinely good here (called out because it breaks the family pattern)

  • The aggregate dimension gap is fixed. Every sibling's reject_metric_style_* rejected saved_metric/sql_expression but missed aggregate. This validator uses col.is_metric (schemas.py:751 → bool(aggregate) or saved_metric or bool(sql_expression)), which closes the gap, and ships a negative test (test_heatmap_axis_rejects_aggregate) for both axes. This is the only plugin in the family to get that right — nice.
  • The axes are unambiguous. x_axis/y_axis are ColumnRef (plain column references), not AxisConfig styling objects, so this config does not re-create the column-vs-styling ambiguity that #42841's _route_x_axis_key exists to disambiguate. No need to adopt that routing here.

One-line family takeaway

The single fix that would have prevented findings across all seven PRs: have the MCP query path run each viz type's real buildQuery (or a shared server-side equivalent) instead of re-deriving columns/metrics/orderby/post-processing in build_query_dicts_from_form_data. Every family bug — funnel orderby, sankey groupby, bubble metrics, and heatmap's x_axis + rank normalization — is a transform that lives only in buildQuery and is silently lost when the query context is assembled straight from form_data.

@bito-code-review

bito-code-review Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #e91066

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: d255e55..55caf41
    • superset/mcp_service/chart/schemas.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@bito-code-review

bito-code-review Bot commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #28438b

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 55caf41..d2c667c
    • superset/mcp_service/chart/chart_helpers.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@gkneighb

Copy link
Copy Markdown
Contributor Author

@aminghadersohi — fixed #1 (the x_axis drop) in d2c667c8b4: build_query_dicts_from_form_data now folds x_axis into the GROUP BY columns for heatmap_v2 too (it was time-series-only), so both axes reach the query. Added a query-context test asserting day_of_week and hour both appear in the built columns.

On #2 (normalize_across → rankOperator post-processing): that's a real gap but a heavier fix (replicating the post-processing operator), and lower severity since the cells still render, just un-normalized — I've noted it as a follow-up rather than bundling it here.

Agreed on the family synthesis — the durable fix is routing the MCP path through the real buildQuery / a shared server-side equivalent rather than re-deriving per chart. Happy to open a tracking issue for that.

@aminghadersohi aminghadersohi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at d2c667c8b4.

#1 (x_axis dropped from GROUP BY) — fixed, verified. Ran the query builder against the new HEAD: build_query_dicts_from_form_data now yields columns=['day_of_week', 'hour'], so both axes reach GROUP BY. The is_timeseries or viz_type == "heatmap_v2" fold is correct and the new test_x_axis_reaches_group_by guards it. Good.

#2 (normalize_across → rankOperator post-processing) — acknowledged/deferred, no objection. Agreed it's lower severity and a heavier fix; fine as a tracked follow-up.

Net-new nit — chart_type enumerations omit heatmap_v2 (discoverability). This PR registers heatmap_v2 in get_chart_type_schema.py's adapter map and examples, but the same file's tool docstring still lists the old set:

get_chart_type_schema.py (~L252): Valid chart_type values: xy, table, pie, pivot_table, mixed_timeseries, handlebars, big_number, histogram, box_plot, waterfall. — missing heatmap_v2.

The addition also makes two adjacent MCP-facing docstrings stale (not edited by this PR, but now inaccurate because heatmap is a generate_chart-supported type):

  • generate_chart.py (~L87): MUST include chart_type in config (one of: 'xy', ... 'waterfall') — omits heatmap_v2.
  • app.py (~L420): claims display_name is populated only for "the 10 chart types supported by generate_chart (…waterfall)" and lists Heatmap under "all other viz_types … it will be null" — heatmap is now supported, so both halves are wrong.

Low severity (the adapter/examples make it functional), but these strings are what an LLM reads to discover valid types. Worth updating at least the in-file docstring.

@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from d2c667c to 3e7e2f3 Compare August 31, 2026 15:21

@aminghadersohi aminghadersohi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 3e7e2f369c (rebased onto latest master since round 2).

  • x_axis GROUP BY fix — still correct after the rebase. Re-ran the query builder at this head: build_query_dicts_from_form_data yields columns=['day_of_week', 'hour'], both axes present. No regression from the rebase.
  • Docstring nit — resolved. get_chart_type_schema's docstring now lists heatmap_v2 among the core chart_type values.
  • normalize_across → rankOperator post-processing — still open, as agreed. Confirmed the built query still has no post_processing; tracked as your follow-up, not a blocker here.

No new findings this round.

@bito-code-review

bito-code-review Bot commented Aug 31, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #274e20

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/mcp_service/chart/schemas.py - 1
    • Metric not validated · Line 1069-1073
      `metric` is a required `ColumnRef` but nothing enforces it is a metric. Passing `metric={"name":"trips"}` (no aggregate/saved_metric) passes validation, and `create_metric_object` silently defaults the aggregate to `SUM`, producing `SUM(trips)` without the user asking. `BigNumberChartConfig.validate_metric_aggregate` rejects this case; consider the same guard here.
  • superset/mcp_service/chart/plugins/heatmap.py - 1
    • No-op pass branch · Line 116-117
      The `if config_dict["metric"].get("sql_expression"): pass` branch is a no-op that only exists to skip canonicalization for SQL-expression metrics. Invert the condition (`if not ...sql_expression`) and nest the `saved_metric`/else logic inside to remove the empty `pass` and make the intent explicit.
Review Details
  • Files reviewed - 7 · Commit Range: 775e1bc..3e7e2f3
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@aminghadersohi

Copy link
Copy Markdown
Contributor

@gkneighb Thanks—the prior X-axis GROUP BY and discoverability fixes remain correct at 3e7e2f369c. An expanded cross-family customer audit found additional Heatmap product-path gaps that were outside my earlier mapper/query review. Before we count typed Heatmap support as end-to-end complete, could you please address or coordinate these?

  1. Native round-trip and updates: adapt returned native string axes plus saved/SIMPLE/SQL metric objects back into HeatmapChartConfig, and preserve omitted axis/presentation/filter state across immediate, preview-first, and cached updates while honoring explicit values and empty-filter clears.
  2. Axis semantics: require X and Y to resolve to distinct canonical columns and fail on ambiguous case-insensitive matches. Wire the frontend sort_x_axis/sort_y_axis behavior and close the previously acknowledged normalize_across → rankOperator post-processing gap rather than silently accepting settings the MCP query does not apply.
  3. Heatmap-faithful previews: saved and unsaved paths currently fall through to unrelated generic scatter/bar behavior. Add one shared rect/cell representation using both axes and the resolved metric label, with all-row, empty, malformed, and query-error validation—or return a clear unsupported-format error instead of a false chart.
  4. Presentation + product tests: expose/map the meaningful frontend Heatmap controls and add generate/update/update-preview/get-data plus saved/form-data-key and unsaved preview coverage, not only model/mapper/registry assertions.

The existing head remains green and the original focused tests pass; these are end-to-end contract gaps rather than regressions in your X-axis fix. Shared adapters, update preservation, result-envelope validation, and preview dispatch should be reused from the coordinated MCP infrastructure work rather than duplicated per chart. I did not modify or push to your branch.

@rusackas

rusackas commented Sep 4, 2026

Copy link
Copy Markdown
Member

@aminghadersohi your call... merge, or block/wait for more updates here?

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 6, 2026
@rusackas
rusackas force-pushed the feat/mcp-heatmap-plugin branch from 3e7e2f3 to e8b2701 Compare September 7, 2026 02:37
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Sep 7, 2026
@bito-code-review

bito-code-review Bot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #14dca6

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/mcp_service/chart/plugins/heatmap.py - 1
    • Empty no-op branch · Line 117-117
      The `if ... get("sql_expression"): pass` branch at line 117 is an empty no-op that only exists to structure the elif chain. It adds confusion (readers may assume it does something) and is dead logic. Invert the condition to `if not ...get("sql_expression"):` and drop the `pass`. Note `waterfall.py` has the same pattern, so consider aligning both.
Review Details
  • Files reviewed - 7 · Commit Range: 7b71aad..e8b2701
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@gabotorresruiz gabotorresruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Greg, and thanks Amin for the earlier rounds. The GROUP BY fold fix and the is_metric axis validator are both solid, and I confirmed the suites are green at this head (1446 passed across the MCP chart directory and the form-data query-context tests).

To Evan's merge-or-wait question: wait, I am afraid. I have to request changes for one defect the earlier rounds could not have seen, because it lives in a different query builder than the one they traced: the compile check inside generate_chart crashes with an uncaught AttributeError on the scalar groupby this mapper emits, so the tool cannot create a heatmap at all. Repro and suggested fix in the inline comment; the same call with a waterfall config degrades gracefully. The columns_from_form_data coercion it needs is a couple of lines and also fixes a latent export-path crash for migrated heatmap charts.

Unlike the broader end-to-end items Amin listed on Sep 1, which read to me like fine follow-ups, this one is a hard runtime failure of the feature the PR adds, so I would fold it into this PR. The normalize_across comment is a cheap addition to the already-agreed follow-up. And a gentle nudge that two of the three doc spots from the earlier round (generate_chart.py docstring and the app.py instructions block, which still lists Heatmap as a type whose display name will be null) are still pending.

form_data: Dict[str, Any] = {
"viz_type": "heatmap_v2",
"x_axis": config.x_axis.name,
"groupby": config.y_axis.name,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Greg, one more query-path issue, and I am afraid this one is a blocker: generate_chart still cannot produce a heatmap on this branch. It is a different builder than the one fixed for the GROUP BY fold.

The compile check that runs on every generate_chart call (both the save path and the preview-only path) builds its columns through preview_utils._build_query_columns, which delegates to columns_from_form_data (superset/common/form_data_query_context.py:151). That helper calls .copy() on form_data["groupby"], and this mapper emits it as a bare string, so it raises AttributeError: 'str' object has no attribute 'copy'. Neither _compile_chart's except clauses nor the tool's outer handler catch AttributeError, so the whole tool call blows up.

I verified it at this head (e8b270167a), in a real app context:

fd = map_heatmap_config(HeatmapChartConfig(
    chart_type="heatmap_v2",
    x_axis={"name": "day_of_week"},
    y_axis={"name": "hour"},
    metric={"name": "trips", "aggregate": "COUNT"},
))
_compile_chart(fd, 1)
# AttributeError: 'str' object has no attribute 'copy'

The same call with a waterfall config returns a structured CompileResult instead of raising.

The scalar itself is the faithful shape (the groupby control is multi: false, and MigrateHeatmapChart renames the scalar all_columns_y straight to groupby), so I would fix the shared helper rather than this mapper: coerce a string groupby into a one-element list inside columns_from_form_data, mirroring what chart_helpers.resolve_groupby already does and what your GROUP BY fold fix effectively assumes. That also fixes the same latent crash for migrated heatmap charts in the dashboard Excel export path, which reaches this helper via _columns_and_metrics.

For tests: one case in tests/unit_tests/common/test_form_data_query_context.py with {"x_axis": "day", "groupby": "hour"} expecting ["day", "hour"], plus a sibling of your test_x_axis_reaches_group_by that goes through columns_from_form_data instead of chart_helpers, since the two builders do not share code. That split is exactly why the suite stays green with this crash present. Happy to dig in with you if it does not reproduce on your side.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced exactly — columns_from_form_data raised AttributeError: 'str' object has no attribute 'copy' on the scalar groupby, and neither _compile_chart nor the tool's outer handler caught it. Fixed in the shared helper as you suggested rather than in the mapper, since the scalar is the faithful shape (groupby is multi: false, and MigrateHeatmapChart renames all_columns_y straight to the scalar).

columns_from_form_data now runs a _as_column_list coercion on groupby, columns, and the raw-mode branch, wrapping a scalar in a one-element list, mirroring chart_helpers.resolve_groupby. That also removes the latent crash on the dashboard Excel export path for migrated heatmap charts, which reaches the same helper via _columns_and_metrics.

Tests: tests/unit_tests/common/test_form_data_query_context.py gets test_columns_scalar_groupby_is_coerced_to_list ({"x_axis": "day", "groupby": "hour"} → ["day", "hour"]) and a scalar-columns sibling; the heatmap suite gets test_x_axis_reaches_columns_from_form_data, which goes through columns_from_form_data rather than chart_helpers so the two builders are both guarded.

description="Value metric colouring each cell (use aggregate e.g. SUM, "
"COUNT for ad-hoc, or set saved_metric=True for a saved dataset metric)",
)
normalize_across: Literal["heatmap", "x", "y"] = Field(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building on the normalize_across post-processing gap already agreed as a follow-up: there is a second half that is cheap to fix now. Even on the path where the frontend buildQuery does run (a saved chart rendered in Explore or a dashboard), the rank column is computed but never used for color, because transformProps.ts has colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel and the normalized checkbox defaults to false and is not exposed here. So today a caller setting normalize_across sees no visual difference anywhere.

Exposing normalized: bool = False in this config and passing it through the mapper makes the knob real on the frontend path immediately, independent of the heavier server-side work. A mapping test asserting form_data["normalized"] would lock it in, and this field's description should mention it only takes effect with normalized=true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. HeatmapChartConfig now exposes normalized: bool = False, and the mapper threads it into form_data, so normalize_across is no longer inert on the frontend path — with normalized=true the rank column becomes colorColumn. Both field descriptions now state that normalize_across only takes effect when normalized=true. Mapping tests assert the default (False) and the pass-through (True).

The server-side rankOperator post-processing remains the tracked follow-up, as agreed.

return self


class HeatmapChartConfig(BaseChartConfig):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question, not a blocker: any reason to leave out time_grain? The heatmap control panel's Query section includes time_grain_sqla, and the waterfall config exposes time_grain with the granularity_sqla mirroring. The PR text frames the deferred fields as cosmetic, but time grain is part of the query contract; a heatmap with a temporal x_axis (say month vs region) cannot be bucketed without it. Fine as a follow-up if intentional.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, deferred. This PR keeps the field set minimal (the stated scope), and time_grain only bites with a temporal x_axis. It is a fair query-contract gap, so I will add it in the same follow-up as the normalize_across server-side work, matching the granularity_sqla mirroring waterfall already does — unless you would prefer it folded in here.

@aminghadersohi

Copy link
Copy Markdown
Contributor

@rusackas @gkneighb One independent re-review at e8b270167a4b5a3125fa72bdee66b03afb4178d2: wait on the runtime failure. The Heatmap patches are effectively unchanged after the rebase; the earlier axis/GROUP BY and schema-discovery fixes remain correct.

I independently reproduced Gabo’s compile finding through the registered FastMCP tools, with authentication/dataset fixtures and no chart writes:

  • generate_chart({"dataset_id":1,"config":{"chart_type":"heatmap_v2","x_axis":{"name":"day"},"y_axis":{"name":"hour"},"metric":{"name":"trips","aggregate":"COUNT"}},"save_chart":false}) raises 'str' object has no attribute 'copy'; save_chart:true does too.
  • The same config in update_chart with identifier:1 fails with structured AttributeError for both generate_preview:false and true. Please cover all four public paths when coercing scalar groupby in the shared column builder.

The September 1 product-path requests also remain open, independently of that blocker:

  • Mapper output fails typed native round-trip. Update-builder probes reset omitted normalize_across:"y"/row_limit:42 to "heatmap"/10000; immediate replacement additionally drops saved presentation/filters/template parameters, unlike preview-first merging. Preserve omitted fields consistently; explicit values and empty-filter clears should win.
  • Public saved/key/combined-key Vega previews produce circle, not rect, marks. With query execution mocked, previews/data also accept rows alongside status:"failed",error:"database error", missing metric values, or a nonnumeric last-row metric. Use shared Heatmap cell/result validation, or explicitly reject unsupported previews.
  • Sorting/rank normalization and distinct/unambiguous canonical axes remain unimplemented.

Checks: focused chart/query suites 1,446 passed; common/chart-data API 350 passed; 51 temporary Heatmap probes plus 9 Jinja-context checks. Broader MCP suite: 3,739 passed, 1 health-check smoke failure, independently reproduced on upstream master c45fc6a31a2e. Ruff, mypy, and applicable pre-commit hooks passed.

These are remaining contract gaps, not regressions in the original GROUP BY fix. No branch edits/pushes, approval, or thread resolutions.

@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from e8b2701 to c9cc30e Compare September 9, 2026 15:41
gkneighb pushed a commit to gkneighb/superset that referenced this pull request Sep 9, 2026
Addresses gabotorresruiz's review of the heatmap plugin (apache#43570).

Blocker: generate_chart could not produce a heatmap at all. The compile check
derives columns via columns_from_form_data (a different builder than the
GROUP BY fold fixed earlier), which called .copy() on form_data['groupby'].
The heatmap Y axis is a single-select control, so the mapper emits groupby as
a bare string, raising 'str' object has no attribute 'copy' — uncaught by
_compile_chart, so the whole tool call blew up. Fixed in the shared helper:
_as_column_list coerces a scalar groupby/columns into a one-element list,
mirroring chart_helpers.resolve_groupby. This also fixes the latent crash for
migrated heatmap charts (MigrateHeatmapChart renames the scalar all_columns_y
straight to groupby) in the dashboard Excel export path, which reaches the same
helper. Tests cover the scalar cases directly and through the heatmap mapper.

normalized: expose normalized (default False) and thread it through the mapper.
normalize_across has no visual effect on the frontend unless normalized is set
(transformProps colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel), so
without this the knob was inert. Descriptions updated to say so.

Docs: app.py instructions block no longer lists Heatmap among the viz_types
whose display_name is null — heatmap_v2 is a registered plugin, so its display
name resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 11, 2026
@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from aea6824 to 1783507 Compare September 19, 2026 02:52
gkneighb pushed a commit to gkneighb/superset that referenced this pull request Sep 19, 2026
Addresses gabotorresruiz's review of the heatmap plugin (apache#43570).

Blocker: generate_chart could not produce a heatmap at all. The compile check
derives columns via columns_from_form_data (a different builder than the
GROUP BY fold fixed earlier), which called .copy() on form_data['groupby'].
The heatmap Y axis is a single-select control, so the mapper emits groupby as
a bare string, raising 'str' object has no attribute 'copy' — uncaught by
_compile_chart, so the whole tool call blew up. Fixed in the shared helper:
_as_column_list coerces a scalar groupby/columns into a one-element list,
mirroring chart_helpers.resolve_groupby. This also fixes the latent crash for
migrated heatmap charts (MigrateHeatmapChart renames the scalar all_columns_y
straight to groupby) in the dashboard Excel export path, which reaches the same
helper. Tests cover the scalar cases directly and through the heatmap mapper.

normalized: expose normalized (default False) and thread it through the mapper.
normalize_across has no visual effect on the frontend unless normalized is set
(transformProps colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel), so
without this the knob was inert. Descriptions updated to say so.

Docs: app.py instructions block no longer lists Heatmap among the viz_types
whose display_name is null — heatmap_v2 is a registered plugin, so its display
name resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Sep 19, 2026

@bito-code-review bito-code-review Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #15921f

Actionable Suggestions - 5
  • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py - 3
  • superset/mcp_service/chart/schemas.py - 1
  • superset/mcp_service/chart/plugins/heatmap.py - 1
    • Missing method docstrings (BITO 12147) · Line 45-150
Additional Suggestions - 2
  • superset/mcp_service/chart/tool/get_chart_type_schema.py - 1
    • Garbled docstring fragment · Line 352-352
      The docstring rewrite left a duplicated fragment on line 352 ('pivot extension also expose interactive_pivot.') that repeats lines 350-351. This is the LLM-facing contract for `get_chart_type_schema`; the garbled text can confuse clients. Remove the leftover line.
  • superset/mcp_service/chart/schemas.py - 1
    • Redundant always-true guard · Line 1509-1509
      `col` is a required, non-optional `ColumnRef` and `ColumnRef` defines no `__bool__`/`__len__` (schemas.py:715-838), so `if col and ...` is always true here; sibling `TreemapChartConfig.reject_metric_style_groupby` uses `col.is_metric` directly. Drop the redundant guard for consistency.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • superset/mcp_service/chart/plugins/heatmap.py - 1
Review Details
  • Files reviewed - 11 · Commit Range: 6c801af..1783507
    • superset/common/form_data_query_context.py
    • superset/mcp_service/app.py
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/common/test_form_data_query_context.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

class TestHeatmapChartConfigSchema:
"""HeatmapChartConfig schema validation."""

def test_basic_heatmap_config(self) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test docstrings

BITO adaptive rule 12148 requires docstrings on all new test functions. 20 of 22 test methods here (e.g. test_basic_heatmap_config, test_heatmap_missing_x_axis, test_x_axis_reaches_group_by) lack them; only test_heatmap_axis_rejects_aggregate and test_groupby_alias_for_y_axis comply. Sibling test files share the gap, but the rule is org-mandated. Add one-line docstrings stating scenario and expected outcome.

Code Review Run #15921f


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

heatmap_v2 needs an explicit fold or its X dimension is dropped.
"""

def test_x_axis_reaches_group_by(self, monkeypatch) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Untyped monkeypatch fixture

BITO rule 11810 requires fixture-injected parameters to be typed. monkeypatch is untyped at line 212; 24 of 26 monkeypatch parameters under tests/unit_tests/mcp_service annotate it as pytest.MonkeyPatch (the only other exception is pre-existing test_gauge_chart.py:319). Annotate for consistency and static type coverage.

Code Review Run #15921f


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

"""

def test_x_axis_reaches_group_by(self, monkeypatch) -> None:
from superset.mcp_service.chart import chart_helpers

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline imports in tests

BITO rule 12745 requires module-level imports absent a documented circular dependency. Seven test methods import inline: chart_helpers (213), columns_from_form_data (237), registry (253, 265), display_name_for_viz_type (260), _VIZ_CATEGORY (279), _CHART_TYPE_ADAPTERS (284) — none with a justification comment. Hoist them to the top import block.

Code Review Run #15921f


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Comment on lines +1478 to +1482
metric: ColumnRef = Field(
...,
description="Value metric colouring each cell (use aggregate e.g. SUM, "
"COUNT for ad-hoc, or set saved_metric=True for a saved dataset metric)",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metric role not enforced

Unlike GaugeChartConfig and BigNumberChartConfig (schemas.py:1328, schemas.py:2140), this validator never checks self.metric.is_metric, so metric={"name": "trips"} passes validation and create_metric_object silently defaults the aggregate to SUM (chart_utils.py:1004) — or surfaces a confusing DB error for non-numeric columns. Add the sibling is_metric check.

Code Review Run #15921f


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Comment on lines +45 to +150
def pre_validate(
self,
config: dict[str, Any],
) -> ChartGenerationError | None:
missing_fields = []

if "x_axis" not in config:
missing_fields.append("'x_axis' (column along the X axis)")
if "y_axis" not in config and "groupby" not in config:
missing_fields.append("'y_axis' (column along the Y axis)")
if "metric" not in config:
missing_fields.append("'metric' (value colouring each cell)")

if missing_fields:
return ChartGenerationError(
error_type="missing_heatmap_fields",
message=(
f"Heatmap chart missing required fields: "
f"{', '.join(missing_fields)}"
),
details=(
"Heatmaps plot a metric across two dimensions — one on the "
"x_axis and one on the y_axis — colouring each cell by the "
"metric value"
),
suggestions=[
"Add 'x_axis': {'name': 'day_of_week'}",
"Add 'y_axis': {'name': 'hour'}",
"Add 'metric': {'name': 'trips', 'aggregate': 'COUNT'}",
"Example: {'chart_type': 'heatmap_v2', "
"'x_axis': {'name': 'day_of_week'}, "
"'y_axis': {'name': 'hour'}, "
"'metric': {'name': 'trips', 'aggregate': 'COUNT'}}",
],
error_code="MISSING_HEATMAP_FIELDS",
)

return None

def extract_column_refs(self, config: Any) -> list[ColumnRef]:
if not isinstance(config, HeatmapChartConfig):
return []
refs: list[ColumnRef] = [config.x_axis, config.y_axis, config.metric]
if config.filters:
for f in config.filters:
refs.append(ColumnRef(name=f.column))
return refs

def to_form_data(
self, config: Any, dataset_id: int | str | None = None
) -> dict[str, Any]:
return map_heatmap_config(config)

def generate_name(self, config: Any, dataset_name: str | None = None) -> str:
what = _heatmap_chart_what(config)
context = _summarize_filters(config.filters)
return self._with_context(what, context)

def resolve_viz_type(self, config: Any) -> str:
return "heatmap_v2"

def normalize_column_refs(self, config: Any, dataset_context: Any) -> Any:
config_dict = config.model_dump()

for key in ("x_axis", "y_axis"):
col = config_dict.get(key)
if col and not col.get("sql_expression") and not col.get("saved_metric"):
col["name"] = DatasetValidator.get_canonical_column_name(
col["name"], dataset_context
)
if config_dict.get("metric"):
if config_dict["metric"].get("sql_expression"):
pass
elif config_dict["metric"].get("saved_metric"):
config_dict["metric"]["name"] = (
DatasetValidator.get_canonical_metric_name(
config_dict["metric"]["name"], dataset_context
)
)
else:
config_dict["metric"]["name"] = (
DatasetValidator.get_canonical_column_name(
config_dict["metric"]["name"], dataset_context
)
)
DatasetValidator.normalize_filters(config_dict, dataset_context)
return HeatmapChartConfig.model_validate(config_dict)

def schema_error_hint(self) -> ChartGenerationError | None:
return ChartGenerationError(
error_type="heatmap_validation_error",
message="Heatmap chart configuration validation failed",
details=(
"The heatmap chart configuration is missing required "
"fields or has invalid structure"
),
suggestions=[
"Ensure 'x_axis' and 'y_axis' each have a 'name'",
"Ensure 'metric' field has 'name' and 'aggregate'",
"Example: {'chart_type': 'heatmap_v2', "
"'x_axis': {'name': 'day_of_week'}, "
"'y_axis': {'name': 'hour'}, "
"'metric': {'name': 'trips', 'aggregate': 'COUNT'}}",
],
error_code="HEATMAP_VALIDATION_ERROR",
)

@bito-code-review bito-code-review Bot Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing method docstrings (BITO 12147)

None of the seven overridden methods carries an inline docstring; their contracts live only on BaseChartPlugin/ChartTypePlugin. BITO adaptive rule 12147 requires a docstring on every newly introduced function. Brief per-method docstrings (even one line noting heatmap-specific behavior, e.g. the y_axis/groupby alias handling in pre_validate) keep this file self-describing.

Code Review Run #15921f

Duplicated metric normalization

The metric-normalization block (sql_expression pass, saved_metric -> get_canonical_metric_name, else get_canonical_column_name) is copied verbatim from PieChartPlugin and TreemapChartPlugin (and near-verbatim GaugeChartPlugin). A future fix to metric normalization must be applied in four files; extracting one shared helper removes that divergence risk.

Code Review Run #c21aa0


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@gkneighb

Copy link
Copy Markdown
Contributor Author

Heads-up for anyone reviewing the red check here: the three failing checks are not from this diff.

All three come from the same six tests in tests/unit_tests/commands/test_base_restore_version_command.py, which fail on current master for every PR built against it — including PRs from other authors (e.g. #44435). This branch touches nothing under superset/commands/. Filed with the root cause and a suggested fix as #44436.

Everything else on this run is green (61 passed, 3 failed, 0 cancelled), and the branch is rebased onto master and mergeable.

@gabotorresruiz gabotorresruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gkneighb. Re-reviewed at 178350704c, since the head moved after my approval at aea6824573.

Holding for one thing only, and it is a one-line delete: the rebase left a duplicated line in the get_chart_type_schema docstring. Details inline.

Everything else is the change I already approved. I compared the full set of lines the PR adds at aea6824573 and at 178350704c, and the only differences are the chart type lists reflowing around gantt and treemap_v2. plugins/heatmap.py, test_heatmap_chart.py and the form_data_query_context.py edit are byte identical between the two.

What I ran at this head:

  • tests/unit_tests/mcp_service/chart/ plus tests/unit_tests/common/test_form_data_query_context.py: 1925 passed, 1 skipped.
  • test_columns_scalar_groupby_is_coerced_to_list and test_columns_scalar_columns_is_coerced_to_list against the parent 128c0c159a: both fail there with AttributeError: 'str' object has no attribute 'copy', so they really do pin the fix.
  • A real create and a real preview through the tool layer against a live SQLite dataset: generate_chart saved a heatmap_v2 chart with x_axis=day_of_week, groupby=model, metric=SUM(fare), and the executed query returned all nine cells, so both axes still reach GROUP BY after the Gantt rewrite of that branch.

The red unit-tests check is not from this diff. All six failures are in tests/unit_tests/commands/test_base_restore_version_command.py (#44436), and master fixed them in 43fee87672, which is newer than this branch's base, so a rebase should turn it green.

Re-request me once that line is gone and I will re-approve right away.

Comment on lines 351 to 352
interactive_pivot.
pivot extension also expose interactive_pivot.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rebase left master's old tail behind here: the new wording already closes the sentence with interactive_pivot. on the line above, so it is now printed twice.

I checked what clients actually receive rather than reading the diff. Booting the MCP app and reading the live description for get_chart_type_schema shows both lines. It is absent from aea6824573, the commit I approved, and absent from 128c0c159a, so it is new at this head.

Suggested change
interactive_pivot.
pivot extension also expose interactive_pivot.
interactive_pivot.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 22, 2026
gkneighb pushed a commit to gkneighb/superset that referenced this pull request Sep 24, 2026
Addresses gabotorresruiz's review of the heatmap plugin (apache#43570).

Blocker: generate_chart could not produce a heatmap at all. The compile check
derives columns via columns_from_form_data (a different builder than the
GROUP BY fold fixed earlier), which called .copy() on form_data['groupby'].
The heatmap Y axis is a single-select control, so the mapper emits groupby as
a bare string, raising 'str' object has no attribute 'copy' — uncaught by
_compile_chart, so the whole tool call blew up. Fixed in the shared helper:
_as_column_list coerces a scalar groupby/columns into a one-element list,
mirroring chart_helpers.resolve_groupby. This also fixes the latent crash for
migrated heatmap charts (MigrateHeatmapChart renames the scalar all_columns_y
straight to groupby) in the dashboard Excel export path, which reaches the same
helper. Tests cover the scalar cases directly and through the heatmap mapper.

normalized: expose normalized (default False) and thread it through the mapper.
normalize_across has no visual effect on the frontend unless normalized is set
(transformProps colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel), so
without this the knob was inert. Descriptions updated to say so.

Docs: app.py instructions block no longer lists Heatmap among the viz_types
whose display_name is null — heatmap_v2 is a registered plugin, so its display
name resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from 1783507 to 8ddb7d1 Compare September 24, 2026 02:18
@gkneighb

Copy link
Copy Markdown
Contributor Author

@gabotorresruiz Fixed at 8ddb7d10e4 — thank you for booting the app and reading the live tool description rather than the diff; that duplicated line was mine, introduced when I resolved the Gantt-cascade rebase.

The cause was the conflict region ending mid-sentence: master's tail line sat outside the conflict markers, and my replacement text re-closed the sentence, so the old tail survived underneath. Gone now, and I audited all four branches in this family for the same mistake — it was on every one of them, and every one is now clean.

While I was here: bubble (#43572) merged on 2026-09-21, which re-conflicted this branch, so this head is also rebased onto current master — which picks up 43fee87672 and should clear the red unit-tests check you correctly attributed to #44436.

Re-verified at this head: _CHART_TYPE_ADAPTERS 16 == _CHART_EXAMPLES 16 with nothing missing, heatmap_v2/bubble_v2/gantt all in the registry, test_heatmap_chart.py + test_form_data_query_context.py passing, ruff/mypy/pylint clean on the committed tree, and git merge-tree clean against master. Re-requesting review.

@bito-code-review

Copy link
Copy Markdown
Contributor

AI Code Review is in progress (usually takes 3 to 15 minutes unless it's a very large PR).

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

@gabotorresruiz gabotorresruiz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gkneighb, and thanks for your patience on this one. Approving, which clears my earlier change request.

I did not take the fix on faith. I booted the MCP app at this head and at the merge base 298aa2f0ba and diffed the live tool surface clients actually receive. All 70 tools are present on both sides, the duplicated interactive_pivot. line is gone from the get_chart_type_schema description, and the only other deltas are the intended ones: the heatmap_v2 additions to the generate_chart description and to the server instructions block, plus the HeatmapChartConfig branch appearing in the generate_chart, update_chart, update_chart_preview and generate_explore_link input schemas. Nothing else in any description or schema moved, and the only removed line across those four schemas is the ChartConfig union description you updated on purpose.

What else I ran at 8ddb7d10e4:

  • tests/unit_tests/mcp_service/chart/ plus tests/unit_tests/common/test_form_data_query_context.py: 2206 passed, 3 skipped.
  • test_columns_scalar_groupby_is_coerced_to_list and test_columns_scalar_columns_is_coerced_to_list copied onto the merge base: both fail there with AttributeError: 'str' object has no attribute 'copy', so they really do pin the fix.
  • A real create, read and preview through the registered MCP tools against a live SQLite dataset: generate_chart saved a heatmap_v2 chart, get_chart_sql returned SELECT day_of_week, hour, sum(fare) ... GROUP BY day_of_week, hour, and get_chart_data came back with all nine cells. normalized and normalize_across both thread into the saved form_data, a saved metric passes through as a bare name, and a config missing y_axis is rejected cleanly instead of crashing.
  • _CHART_TYPE_ADAPTERS and _CHART_EXAMPLES are both 16 with no gap either way, and every one of the 16 is named in both the generate_chart and get_chart_type_schema descriptions.
  • A line by line comparison of the patch at 178350704c and at this head: plugins/heatmap.py, test_heatmap_chart.py and the form_data_query_context.py edit are identical, and the only other change is the chart type list reflowing around bubble_v2.

Every check is green on this run, including unit-tests, so the #44436 failures really were coming from master and the rebase cleared them.

I left one non-blocking note inline about a sibling of the scalar case. Nothing there needs to happen before merge.

Comment on lines +143 to +147
if value is None:
return []
if isinstance(value, str):
return [value]
return list(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker, and not something this PR broke. A note on the sibling of the case you are fixing.

The same multi: false groupby control stores a bare object, not a string, when the Y axis is an adhoc or calculated column: OptionSelector.getValues() returns getColumnNameOrAdhocColumn(values[0]) when multi is false. list(value) on that object yields its keys, so the coercion succeeds and hands the query three invented column names.

I ran build_query_context_from_form_data on a heatmap form_data whose groupby is {"expressionType": "SQL", "sqlExpression": ..., "label": "hour_band"}. At this head it builds columns == ["day_of_week", "expressionType", "sqlExpression", "label"]; at the merge base 298aa2f0ba the same call raises AttributeError: 'dict' object has no attribute 'insert'. So on the dashboard Excel export path it trades a loud crash for a wrong column list. chart_helpers.resolve_groupby, the mirror this docstring cites, has the same blind spot, so this is a pre-existing family gap and not a regression.

ensureIsArray semantics cover both shapes if you want it closed here:

Suggested change
if value is None:
return []
if isinstance(value, str):
return [value]
return list(value)
if value is None:
return []
if isinstance(value, (list, tuple)):
return list(value)
return [value]

plus a test_columns_adhoc_groupby_is_wrapped_not_expanded alongside your two scalar cases. Equally happy for you to call it out of scope, since nothing the MCP mapper emits can reach it.

@rusackas

Copy link
Copy Markdown
Member

If it's not a blocker, I'm inclined to merge. @gkneighb let us know if you want to address @gabotorresruiz's last comment first, thanks!

@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from 8ddb7d1 to 2f6857a Compare September 29, 2026 03:04
@gkneighb

Copy link
Copy Markdown
Contributor Author

@rusackas Thanks — nothing blocking from my side, so it's good to merge whenever you are.

On @gabotorresruiz's last comment: he explicitly flagged it as "not a blocker, and not something this PR broke". It's the sibling of the case this PR fixes — a multi: false groupby also stores a bare object (not just a string) when the Y axis is an adhoc or calculated column, so the coercion hands the query invented column names. Real, but pre-existing on master and orthogonal to the scalar-string fix here. I'd rather it get its own PR with its own test than be smuggled in.

One thing I did push since your comment, at 2f6857a982: the exclude_unset=True fix you asked for on #43573. heatmap.py had the identical shape, so the same one-word change plus a test_merge_heatmap_preserves_omitted_row_limit regression is now on this branch. (Heatmap has no color_scheme control, so only the row limit applies.) I audited all the registered plugins for it — details on #43573.

CI is green at this head.

@bito-code-review bito-code-review Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #c21aa0

Actionable Suggestions - 2
  • superset/mcp_service/chart/plugins/heatmap.py - 1
  • superset/common/form_data_query_context.py - 1
Additional Suggestions - 1
  • tests/unit_tests/mcp_service/chart/test_chart_utils.py - 1
    • Inline imports vs module-level rule · Line 126-127
      These are the only function-body `from superset` imports in this 2,600-line file; `chart_utils` and `chart.schemas` are already imported at module level (lines 26-56), so no circular-dependency rationale applies. BITO.md rule 12745 requires module-level imports unless a documented circular dependency exists. Move `map_heatmap_config` and `HeatmapChartConfig` into the existing top-level import blocks.
Review Details
  • Files reviewed - 12 · Commit Range: 2363c59..1147bf7
    • superset/common/form_data_query_context.py
    • superset/mcp_service/app.py
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/common/test_form_data_query_context.py
    • tests/unit_tests/mcp_service/chart/test_chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +133 to +147
def _as_column_list(value: Any) -> list[Any]:
"""
Normalize a ``groupby``/``columns`` value into a list.

Single-select controls (e.g. the heatmap ``groupby`` Y axis, which is
``multi: false``, and heatmap charts migrated via ``MigrateHeatmapChart``)
store the dimension as a bare string. Wrap a scalar in a one-element list,
mirroring ``chart_helpers.resolve_groupby``, so downstream list operations
(``.copy()``, ``.insert()``) do not blow up on a ``str``.
"""
if value is None:
return []
if isinstance(value, str):
return [value]
return list(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated column-list helper

This new helper duplicates _as_column_list in superset/mcp_service/chart/chart_utils.py:2416 (same name, same purpose) with divergent edge semantics: that version wraps any non-list scalar, while this one calls list(value), which raises TypeError on a non-iterable scalar. as_list from superset.utils.core is already imported in this module. Consolidate in a shared util to prevent divergence. ([CWE not applicable])

Citations

Code Review Run #c21aa0


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Sep 30, 2026
Greg Neighbors and others added 3 commits October 1, 2026 23:34
Adds a generate_chart plugin for heatmap (viz_type 'heatmap_v2'): an x_axis
column and a single groupby (Y) column form the two axes, one metric colours
the cells, and normalize_across selects the rank-normalization range.

Rebased onto current master (past the gauge plugin + follow-up). Folds x_axis
into the GROUP BY columns for heatmap_v2 in build_query_dicts_from_form_data
(was time-series-only, so the X dimension was dropped). Adds the
generate_chart.py docstring entry (one-of, per-type bullet, 'heatmap'/'density
grid' quick-lookup) + the get_chart_type_schema core list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses gabotorresruiz's review of the heatmap plugin (apache#43570).

Blocker: generate_chart could not produce a heatmap at all. The compile check
derives columns via columns_from_form_data (a different builder than the
GROUP BY fold fixed earlier), which called .copy() on form_data['groupby'].
The heatmap Y axis is a single-select control, so the mapper emits groupby as
a bare string, raising 'str' object has no attribute 'copy' — uncaught by
_compile_chart, so the whole tool call blew up. Fixed in the shared helper:
_as_column_list coerces a scalar groupby/columns into a one-element list,
mirroring chart_helpers.resolve_groupby. This also fixes the latent crash for
migrated heatmap charts (MigrateHeatmapChart renames the scalar all_columns_y
straight to groupby) in the dashboard Excel export path, which reaches the same
helper. Tests cover the scalar cases directly and through the heatmap mapper.

normalized: expose normalized (default False) and thread it through the mapper.
normalize_across has no visual effect on the frontend unless normalized is set
(transformProps colorColumn = normalized ? RANK_COLUMN_NAME : metricLabel), so
without this the knob was inert. Descriptions updated to say so.

Docs: app.py instructions block no longer lists Heatmap among the viz_types
whose display_name is null — heatmap_v2 is a registered plugin, so its display
name resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gkneighb
gkneighb force-pushed the feat/mcp-heatmap-plugin branch from 1147bf7 to 7b09f5c Compare October 2, 2026 03:34
@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 2, 2026

@bito-code-review bito-code-review Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Agent Run #e0a661

Actionable Suggestions - 3
  • tests/unit_tests/mcp_service/chart/test_chart_utils.py - 2
  • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py - 1
Additional Suggestions - 1
  • superset/mcp_service/chart/schemas.py - 1
    • Duplicated dimension validator · Line 1777-1788
      `HeatmapChartConfig.reject_metric_style_dimensions` duplicates `BubbleChartConfig.reject_metric_style_dimensions` (schemas.py:1698-1712): identical loop over dimension `ColumnRef`s calling `_reject_sql_expression_on_dimension` then checking `col.is_metric`. Extract a shared helper to keep the two dimension-rejection rules in one place; divergence here would let one chart type accept a metric-style dimension the other rejects.
Review Details
  • Files reviewed - 12 · Commit Range: 27126b3..7b09f5c
    • superset/common/form_data_query_context.py
    • superset/mcp_service/app.py
    • superset/mcp_service/chart/chart_helpers.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/plugins/__init__.py
    • superset/mcp_service/chart/plugins/heatmap.py
    • superset/mcp_service/chart/schemas.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/get_chart_type_schema.py
    • tests/unit_tests/common/test_form_data_query_context.py
    • tests/unit_tests/mcp_service/chart/test_chart_utils.py
    • tests/unit_tests/mcp_service/chart/test_heatmap_chart.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +181 to +182
from superset.mcp_service.chart.chart_utils import map_heatmap_config
from superset.mcp_service.chart.schemas import HeatmapChartConfig

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline duplicate imports

map_heatmap_config and HeatmapChartConfig are already imported at module level (lines 26, 47); these function-body imports duplicate them with no circular-dependency justification or explanatory comment, contrary to the repo rule requiring module-level imports. Extend the existing top-level import lists instead.

Code Review Run #e0a661


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them


merged = merge_chart_form_data(existing, new_form_data, config)

assert {key: merged[key] for key in expected} == expected

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weak merge assertion

The dict-comprehension assertion only inspects keys in expected, so a merge_chart_form_data regression that dropped the update fields (metric, groupby) would still pass. Sibling test_merge_chart_preserves_omitted_defaults also asserts merged["metric"] == new_form_data["metric"]; mirror that here so the merge overlay contract is actually validated.

Code Review Run #e0a661


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Comment on lines +37 to +243
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)
assert config.x_axis.name == "day_of_week"
assert config.y_axis.name == "hour"
assert config.normalize_across == "heatmap" # frontend default

def test_heatmap_missing_x_axis(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)

def test_heatmap_missing_y_axis(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
metric={"name": "trips", "aggregate": "COUNT"},
)

def test_heatmap_missing_metric(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
)

def test_heatmap_rejects_extra_fields(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
bogus=1,
)

def test_heatmap_axis_rejects_aggregate(self) -> None:
"""An aggregate makes an axis metric-like; x_axis/y_axis are dims."""
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week", "aggregate": "COUNT"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour", "aggregate": "COUNT"},
metric={"name": "trips", "aggregate": "COUNT"},
)

def test_heatmap_y_axis_rejects_saved_metric(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "count", "saved_metric": True},
metric={"name": "trips", "aggregate": "COUNT"},
)

def test_heatmap_invalid_normalize_across_rejected(self) -> None:
with pytest.raises(ValidationError):
HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
normalize_across="diagonal",
)

def test_groupby_alias_for_y_axis(self) -> None:
"""Superset-native 'groupby' is accepted for the Y-axis field."""
config = HeatmapChartConfig.model_validate(
{
"chart_type": "heatmap_v2",
"x_axis": {"name": "day_of_week"},
"groupby": {"name": "hour"},
"metric": {"name": "trips", "aggregate": "COUNT"},
}
)
assert config.y_axis.name == "hour"

def test_chart_config_union_dispatches_heatmap(self) -> None:
config = TypeAdapter(ChartConfig).validate_python(
{
"chart_type": "heatmap_v2",
"x_axis": {"name": "day_of_week"},
"y_axis": {"name": "hour"},
"metric": {"name": "trips", "aggregate": "COUNT"},
}
)
assert isinstance(config, HeatmapChartConfig)


class TestMapHeatmapConfig:
"""form_data mapping must match the frontend Heatmap buildQuery."""

def test_basic_heatmap_form_data(self) -> None:
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)
form_data = map_heatmap_config(config)
assert form_data["viz_type"] == "heatmap_v2"
assert form_data["x_axis"] == "day_of_week"
# Y axis uses the groupby key as a single column (control is multi:false)
assert form_data["groupby"] == "hour"
assert form_data["metric"]["label"] == "COUNT(trips)"
assert form_data["normalize_across"] == "heatmap"

def test_heatmap_form_data_with_normalize_and_filters(self) -> None:
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
normalize_across="x",
filters=[{"column": "year", "op": "=", "value": 2026}],
)
form_data = map_heatmap_config(config)
assert form_data["normalize_across"] == "x"
assert form_data["adhoc_filters"], "filters must map to adhoc_filters"

def test_normalized_defaults_false(self) -> None:
# normalize_across has no visual effect on the frontend unless the
# 'normalized' flag is also set, so it must be threaded through.
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)
assert map_heatmap_config(config)["normalized"] is False

def test_normalized_true_maps_through(self) -> None:
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
normalize_across="x",
normalized=True,
)
assert map_heatmap_config(config)["normalized"] is True

def test_heatmap_saved_metric_maps_to_name_string(self) -> None:
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "avg_fare", "saved_metric": True},
)
assert map_heatmap_config(config)["metric"] == "avg_fare"


class TestHeatmapQueryContext:
"""The built query must GROUP BY both axes, not just the Y (groupby) column.

map_heatmap_config emits X under 'x_axis' and Y under 'groupby'; the query
builder folds x_axis into the columns only for time-series viz types, so
heatmap_v2 needs an explicit fold or its X dimension is dropped.
"""

def test_x_axis_reaches_group_by(self, monkeypatch) -> None:
from superset.mcp_service.chart import chart_helpers

monkeypatch.setattr(
chart_helpers,
"resolve_datasource_engine",
lambda datasource_id, datasource_type: "base",
)
config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},
)
form_data = map_heatmap_config(config)
queries = chart_helpers.build_query_dicts_from_form_data(form_data, 1, "table")
columns = queries[0]["columns"]
assert "day_of_week" in columns, "x_axis must reach GROUP BY"
assert "hour" in columns, "y_axis must reach GROUP BY"

def test_x_axis_reaches_columns_from_form_data(self) -> None:
# The generate_chart compile check derives columns through a different
# builder (columns_from_form_data, used by the dashboard export path
# too), which must tolerate the scalar 'groupby' this mapper emits and
# carry both axes rather than crashing on str.copy().
from superset.common.form_data_query_context import columns_from_form_data

config = HeatmapChartConfig(
chart_type="heatmap_v2",
x_axis={"name": "day_of_week"},
y_axis={"name": "hour"},
metric={"name": "trips", "aggregate": "COUNT"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repeated config arrange block

The four-line HeatmapChartConfig(chart_type=..., x_axis=..., y_axis=..., metric=...) arrange block repeats ~18 times across the file. test_bubble_chart.py centralizes the identical pattern in a _base() helper (BubbleChartConfig(**_base())); a _heatmap_base(**overrides) helper would localize future schema changes to one site.

Code Review Run #e0a661


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

@gkneighb

gkneighb commented Oct 7, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current master. CI here is down to one failing test, test_dispatchers_do_not_branch_on_registered_chart_types from #44746, flagging chart_helpers: viz_type == "heatmap_v2" — the line I wrote during this rebase adopting master's with_x_axis_column() helper.

The same single test fails on all four chart-plugin PRs (#43567, #43570, #43571, #43573) for the same structural reason: that guard flags dispatcher branches keyed on registered chart types, so registering a new type is enough to turn pre-existing code into a violation. It needs the per-type behavior moved into the new plugin hooks rather than a line edit.

I've written the full analysis and two questions for @rusackas in one place to avoid four parallel threads: #43573 (comment)

Otherwise this branch is rebased, mergeable, and green.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires:rebase Requires rebasing on top of current master size/XL viz:charts:heatmap Related to the Heatmap chart

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants