feat(mcp): radar chart type plugin - #43571
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #43571 +/- ##
==========================================
- Coverage 79.24% 79.23% -0.02%
==========================================
Files 2888 2889 +1
Lines 166446 166525 +79
Branches 38529 38544 +15
==========================================
+ Hits 131908 131950 +42
- Misses 32045 32080 +35
- Partials 2493 2495 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
1dd87ab to
28a7652
Compare
| metrics: List[ColumnRef] = Field( | ||
| ..., | ||
| min_length=1, |
There was a problem hiding this comment.
Suggestion: The schema accepts a single metric, even though the radar plugin describes radar charts as requiring two or more axes. This allows a one-axis configuration to pass validation and reach chart generation as a degenerate radar; enforce the same minimum metric count in the schema and plugin contract. [api mismatch]
Severity Level: Major ⚠️
- ❌ MCP can generate degenerate one-axis radar charts.
- ⚠️ Radar requests contradict the plugin’s documented two-axis contract.
- ⚠️ Users receive an unusable visualization instead of validation guidance.Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset/mcp_service/chart/schemas.py
**Line:** 1060:1062
**Comment:**
*Api Mismatch: The schema accepts a single metric, even though the radar plugin describes radar charts as requiring two or more axes. This allows a one-axis configuration to pass validation and reach chart generation as a degenerate radar; enforce the same minimum metric count in the schema and plugin contract.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
The flagged issue is correct. The Would you like me to implement these changes and check the rest of the PR comments for similar issues? superset/mcp_service/chart/schemas.py superset/mcp_service/chart/plugins/radar.py |
Code Review Agent Run #dc4bdfActionable Suggestions - 0Additional Suggestions - 1
Filtered by Review RulesBito filtered these suggestions based on rules created automatically for your feedback. Manage rules.
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
aminghadersohi
left a comment
There was a problem hiding this comment.
Thanks for this — the radar plugin follows the established MCP chart-plugin template cleanly and is a pleasure to review. RadarChartConfig, map_radar_config, the _radar_chart_what namer, the discriminated-union wiring, the TypeAdapter registration in get_chart_type_schema.py, and the plugin registration are all consistent with the pie/funnel/gauge siblings, and the field descriptions surface well to an agent. Registry integration and form_data mapping are correct, and the tests exercise the happy paths thoroughly. This is a first human review; nothing below blocks the design, and most of it is a shared convention worth applying across all seven sibling PRs (funnel/gauge/treemap/heatmap/radar/bubble/sankey) at once.
I ran the unit tests (15 passed) and constructed configs directly to verify each claim below. RAN = executed, INSPECTED = read-only.
1. Radar-specific — the schema permits a 1-metric radar its own docs call invalid (confirms codeant)
RAN. A single-metric radar passes every gate and reaches form_data generation:
RadarChartConfig(chart_type='radar', metrics=[{'name':'speed','aggregate':'AVG'}]) # -> valid, len 1
plugin.pre_validate({'chart_type':'radar','metrics':[{'name':'speed','aggregate':'AVG'}]}) # -> None (accepted)
map_radar_config(...) # -> {'viz_type':'radar','metrics':['AVG(speed)'], ...} (degenerate single-spoke radar)
The codeant finding is confirmed, and it is sharper than "seems wrong" because the plugin contradicts its own documented contract:
pre_validatedetails (radar.py): "Radar charts plot one axis per metric. Add two or more 'metrics'"- schema field (
schemas.py:1060): "radars read best with 3 or more" - but the enforcement is
metrics: min_length=1, andpre_validateonly rejects empty metrics (if not config.get("metrics")). - and
schema_error_hintthen says the opposite: "Ensure 'metrics' has at least one metric".
So three doc strings say ≥2/≥3 and one says ≥1, while the code enforces ≥1. Downstream this does not throw — form_data generates fine and ECharts renders a degenerate one-axis radar (a spoke, not a polygon). So this is a quality/UX issue (an unusable chart instead of validation guidance), not a crash or a security issue — I'd call it Minor–Major rather than the bot's flat Major. The fix is one line in the existing @model_validator(mode="after"):
if len(self.metrics) < 2:
raise ValueError("radar charts need at least 2 metrics (axes); a single axis is degenerate")and align the min_length=1 / schema_error_hint wording to match.
2. Template-level — "schemas encode field types but not semantic constraints" is confirmed across the family
I read the schema diffs of all six siblings. Every config class already carries a @model_validator(mode="after") — but in every case it does exactly one job: reject a metric-shaped entry sitting in a dimension slot (reject_metric_style_groupby / reject_metric_style_dimensions / reject_metric_style_nodes). None of them encode the chart's semantic constraint:
| PR | documented constraint | enforced? |
|---|---|---|
| #43568 gauge | min_val < max_val |
no (ordering unchecked) |
| #43569 treemap | aggregate marks a metric, not a groupby |
flagged by codeant |
| #43571 radar | ≥2 metric axes | no (this PR) |
So the synthesis holds: the validators check what kind of field each ref is, never the chart-level invariant (arity, ordering). The good news is the fix is cheap and uniform — the mode="after" validator hook is already present in all seven classes; each just needs its one semantic assertion added. A shared convention here saves this author six more rounds. The radar arity check is radar-specific; the pattern of adding it is template-level.
3. Template-level — the seven PRs will merge-conflict on shared files
INSPECTED (gh api .../files patches). Every sibling inserts at the identical anchors:
chart_utils.py:map_<type>_configright aftermap_pie_config(@@ -1017,6), and_<type>_chart_whatafter_pie_chart_what(@@ -1527,6) — all seven target the same two hunks.schemas.py: a new class afterPieChartConfig, plus edits to the sameChartConfigunion list and the same discriminator description string.get_chart_type_schema.py: same_CHART_TYPE_ADAPTERS/_CHART_EXAMPLESblocks.
These are not the "same helper duplicated" — each function is genuinely distinct (per-chart mapping). But because they land on the same lines, whichever merges first will force mechanical rebases on the other six. Worth sequencing the merges, or landing the shared scaffolding once.
4. Radar-specific — the get_chart_data.py +1 line is benign (and effectively a no-op)
INSPECTED. The one line adds "radar": "radar" to _VIZ_CATEGORY, used only by _recommend_visualizations via current_category = _VIZ_CATEGORY.get(viz_type, viz_type) to avoid recommending a chart type the user already has. Two notes:
- It's why radar alone touches this file: the siblings' viz_types (
gauge_chart,funnel,treemap_v2,heatmap_v2) were already in the map;radarwas not. - Because the lookup already defaults to
viz_typeitself,.get("radar", "radar")returns"radar"without this entry — so the line is functionally a no-op. It's harmless and matches the file's self-documenting convention (funnel,waterfall,box_plotalso self-map), so no change needed; just noting it affects no other chart type and is unrelated to the separateget_chart_dataguard being fixed in #43598.
5. Rule 26 — tests are happy-path only; no arity negative test
RAN / INSPECTED. Reverting only the production files, the whole test_radar_chart.py fails to import (RadarChartConfig, map_radar_config don't exist), so the tests are genuinely coupled to the production code. But there is no negative test for the core defect — and worse, several tests positively assert that a 1-metric radar is valid (test_chart_config_union_dispatches_radar, test_radar_form_data_with_series_and_filters, test_radar_metric_accepts_saved_metric all build single-metric configs and expect success). So the suite currently locks in the behavior its own plugin docs call invalid. Once the ≥2 rule lands, add test_radar_single_metric_rejected and flip those fixtures to 2+ metrics. Bad-column handling is inherited from the shared DatasetValidator.get_canonical_column_name, which returns the original name on no match (silent pass-through) — same as the other plugins, not introduced here.
Summary
- The synthesis ("types enforced, semantics not") is confirmed with code across all seven.
- Radar-specific: add a ≥2-metric check to the existing
model_validator; align the contradictory doc strings; add a negative arity test and fix the single-metric happy-path fixtures. - Template-level: apply the same "one semantic assertion in the existing
mode=aftervalidator" convention to the family; expect (mechanical) merge conflicts onchart_utils.py/schemas.py/get_chart_type_schema.pyacross the seven. - The
get_chart_data.pyline is a benign self-map (no-op given the identity default); the single deleted line inschemas.pyis just the discriminator description string gaining'radar'.
The template is sound and worth the other six following — the main gap is that the semantic invariant belongs in the validator, not just the docstring. Nice work; happy to re-look once the arity check lands.
CI at 28a7652d, deduped by latest run per check name: 47 SUCCESS (incl. netlify preview) / 3 NEUTRAL / 12 SKIPPED, no failures or pending.
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
aminghadersohi
left a comment
There was a problem hiding this comment.
Round 2 — re-reviewed at head 5160b0e5. Verified by pinning the worktree to that SHA (clean tree) and running the unit tests (16 passed); RAN = executed, INSPECTED = read-only.
The new commit is a good fix ✅
fix(mcp): radar — reject aggregate on groupby series columns does exactly the right thing:
- RAN.
groupby=[{'name':'model','aggregate':'SUM'}]now raisesValidationError(before this commit it validated and theaggregatewas silently dropped downstream). The switch fromif col.saved_metric:toif col.is_metric:is the correct fix — it reuses the existingColumnRef.is_metricproperty (schemas.py:751), which unifies all three metric markers (aggregate/saved_metric/sql_expression) instead of checking them one at a time. That's the same class of gap worth closing in the sibling PRs'reject_metric_style_*validators —is_metricis the shared, already-in-repo primitive to standardize on across the family. - The error message now names both
aggregateandsaved_metric, and the newtest_radar_groupby_rejects_aggregatelocks the behavior in. Clean, minimal, well-tested.
One item from round 1 is still open — the single-metric / doc-string arity gap
This is codeant's original thread on schemas.py:1062 (still unresolved, not outdated), and it's a distinct issue from the aggregate fix above — so the new commit doesn't cover it. Re-confirmed RAN at 5160b0e5:
RadarChartConfig(chart_type='radar', metrics=[{'name':'speed','aggregate':'AVG'}]) # -> valid, len 1
plugin.pre_validate({'chart_type':'radar','metrics':[{'name':'speed','aggregate':'AVG'}]}) # -> None (accepted)
The plugin's own three contract strings still disagree on the minimum, and the code enforces the loosest of them:
pre_validatedetails (radar.py): "Add two or more 'metrics'"metricsfield description (schemas.py:1062): "radars read best with 3 or more"schema_error_hint(radar.py): "Ensure 'metrics' has at least one metric"- enforced:
min_length=1+pre_validaterejects only empty → 1 is accepted.
A one-axis radar is geometrically degenerate (a spoke, not a polygon); it doesn't throw, it just produces an unusable chart instead of validation guidance. This is a Minor–Major quality issue, not a blocker. The same mode="after" validator you just edited is the natural home for the fix:
if len(self.metrics) < 2:
raise ValueError("radar needs at least 2 metrics (axes); one axis is degenerate")and align the min_length and the three doc strings to one agreed minimum. A negative test (test_radar_single_metric_rejected) plus flipping the current single-metric happy-path fixtures to 2+ metrics would round it out.
Nothing else regressed — radar.py, chart_utils.py, get_chart_data.py, get_chart_type_schema.py, and plugins/__init__.py are unchanged since round 1, and my earlier notes on those still hold (the get_chart_data.py _VIZ_CATEGORY self-map is a benign no-op; the family will still merge-conflict mechanically on the shared insertion anchors). Thanks for the quick turnaround on the aggregate fix.
CI at 5160b0e5, deduped to the latest run per check name: 48 SUCCESS (incl. netlify preview) / 2 NEUTRAL / 9 SKIPPED, no failures or pending.
Code Review Agent Run #759fb1Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Adds a generate_chart plugin for the radar viz type (viz_type 'radar'). Mirrors the frontend Radar buildQuery contract: multiple metrics become the radar axes (indicators), and an optional groupby splits the data into one polygon per category. The query orders by the first metric descending. - RadarChartConfig schema (non-empty metrics required; optional groupby, row_limit, filters, color_scheme) added to the ChartConfig discriminated union and the get_chart_type_schema adapters. groupby entries may not be saved_metric/sql_expression (dimensions, not metrics). - map_radar_config maps the config to form_data; each metric is emitted as a metric object (saved metrics as bare name strings, ad-hoc as SIMPLE/SQL). - RadarChartPlugin registered in the plugin registry; 'radar' added to the recommendation category map (it was absent). - 17 unit tests covering schema validation (non-empty metrics, groupby optional/not-a-metric), union dispatch, form_data mapping (multi-metric), and registry integration. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
reject_metric_style_groupby now gates on ColumnRef.is_metric, so an aggregate on a series column is rejected (previously only saved_metric/sql_expression), per aminghadersohi's review. Adds a negative aggregate test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5160b0e to
6fadbfc
Compare
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6fadbfc to
e168736
Compare
Code Review Agent Run #8d76d0Actionable Suggestions - 0Additional Suggestions - 2
Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
|
@gkneighb Thanks—the earlier Radar arity/groupby work remains fixed at
I ran the 16 focused Radar tests and 1,384 broader MCP chart/schema tests plus the native-shape, implicit-SUM, ambiguity, duplicate-role, and preview reproductions; these gaps remain with green CI. Shared builder, canonicalization, update-preservation, and result-envelope work in #43737/#43679 should be reused/rebased rather than duplicated. I did not modify or push to your branch. |
SUMMARY
Adds a
generate_chartMCP plugin for the radar chart type (viz_type: radar), so the MCPgenerate_charttool can produce radar/spider charts. Radar is a shipped Superset viz type that had no MCP plugin; this closes that gap.The plugin mirrors the frontend Radar
buildQuerycontract: multiplemetricsbecome the radar axes (indicators), and an optionalgroupbysplits the data into one polygon per category (with no groupby the whole dataset is a single polygon). The query orders by the first metric descending.Follows the established plugin pattern (
pie/funnel/gauge/treemap/heatmap/waterfall) — a config schema, amap_*_configmapper, a plugin class, and registration:RadarChartConfig— a non-emptymetricslist required (each metric is one axis); optionalgroupby(series polygons),row_limit(default 10),filters,color_scheme. Added to theChartConfigdiscriminated union and theget_chart_type_schemaadapters.groupbyentries may not besaved_metric/sql_expression(dimensions, not metrics).map_radar_config— maps the config toform_data; each metric is emitted as a metric object (saved metrics as bare name strings, ad-hoc metrics as SIMPLE/SQL adhoc objects).RadarChartPlugin— registered in the plugin registry.radarwas absent from the recommendation category map, so this adds it (its ownradarcategory, consistent with how funnel/gauge/waterfall each carry a distinct category).The field set is intentionally minimal (core query contract). Cosmetic controls (per-metric min/max bounds via
column_config, label type/position, circular vs polygon shape) 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_radar_chart.py— 17 unit tests cover schema validation (non-emptymetrics,groupbyoptional and not-a-metric, saved-metric axis accepted, extra-field rejection),ChartConfigunion dispatch,form_datamapping (multi-metric labels, series groupby, filters, saved-metric passthrough), and registry integration (registration,resolve_viz_type,display_name,pre_validate, recommendation category).To exercise end-to-end: call the
generate_chartMCP tool with{"chart_type": "radar", "metrics": [{"name": "speed", "aggregate": "AVG"}, {"name": "power", "aggregate": "AVG"}, {"name": "range", "aggregate": "AVG"}]}.ADDITIONAL INFORMATION
🤖 Generated with Claude Code