Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions superset/common/form_data_query_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,23 @@ def freeform_where_having(form_data: dict[str, Any]) -> dict[str, str]:
return extras


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)
Comment on lines +143 to +147

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.

Comment on lines +133 to +147

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



def columns_from_form_data(form_data: dict[str, Any]) -> list[Any]:
"""
Derive the query's grouping/raw columns from form data.
Expand All @@ -141,10 +158,10 @@ def columns_from_form_data(form_data: dict[str, Any]) -> list[Any]:
if form_data.get("query_mode") == "raw" and (
form_data.get("all_columns") or form_data.get("columns")
):
return list(form_data.get("all_columns") or form_data.get("columns") or [])
return _as_column_list(form_data.get("all_columns") or form_data.get("columns"))

groupby_columns: list[Any] = form_data.get("groupby") or []
raw_columns: list[Any] = form_data.get("columns") or []
groupby_columns: list[Any] = _as_column_list(form_data.get("groupby"))
raw_columns: list[Any] = _as_column_list(form_data.get("columns"))
# Prefer explicit raw columns only when they are actually present; a stale
# empty ``columns: []`` key must not shadow the group-by dimensions (which
# would silently drop the grouping and change the aggregation).
Expand Down
10 changes: 5 additions & 5 deletions superset/mcp_service/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,11 +448,11 @@ def get_default_instructions(
chart_type_display_name field with a human-readable name when available.
This field is populated for chart types known to the MCP registry
(xy, pie, table, pivot_table, big_number, mixed_timeseries, handlebars,
histogram, box_plot, waterfall, gantt, bubble_v2, and interactive_pivot).
Availability gates creation and schema discovery, not display names for
existing charts.
For all other viz_types (Funnel, Gauge, Heatmap, etc.) it will be null —
use the raw viz_type field instead when referring to those chart types.
histogram, box_plot, waterfall, gantt, bubble_v2, heatmap_v2, and
interactive_pivot). Availability gates creation and schema discovery, not
display names for existing charts. For all other viz_types it will be
null — use the raw viz_type field instead when referring to those chart
types.

Query Examples:
- List all tables:
Expand Down
5 changes: 4 additions & 1 deletion superset/mcp_service/chart/chart_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,10 @@ def build_query_dicts_from_form_data(
qd["filters"] = [*(qd.get("filters") or []), *null_filters]
return [qd]

if viz_type.startswith("echarts_timeseries"):
# Heatmap puts its x_axis in the query columns too: the frontend folds it
# into groupby in buildQuery, and the MCP path builds the query dict
# directly, so without this the x axis never reaches GROUP BY.
if viz_type.startswith("echarts_timeseries") or viz_type == "heatmap_v2":
groupby = with_x_axis_column(form_data, groupby)

return [
Expand Down
30 changes: 30 additions & 0 deletions superset/mcp_service/chart/chart_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
GanttChartConfig,
GaugeChartConfig,
HandlebarsChartConfig,
HeatmapChartConfig,
HistogramChartConfig,
MixedTimeseriesChartConfig,
PieChartConfig,
Expand Down Expand Up @@ -1694,6 +1695,27 @@ def map_bubble_config(config: BubbleChartConfig) -> Dict[str, Any]:
return form_data


def map_heatmap_config(config: HeatmapChartConfig) -> Dict[str, Any]:
"""Map heatmap config to Superset form_data (viz_type ``heatmap_v2``).

Matches the frontend Heatmap buildQuery contract: 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.
The Y axis is a single-select ``groupby`` (not a list).
"""
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.

"metric": create_metric_object(config.metric),
"normalize_across": config.normalize_across,
"normalized": config.normalized,
"row_limit": config.row_limit,
}
_add_adhoc_filters(form_data, config.filters)
return form_data


def map_histogram_config(config: "HistogramChartConfig") -> Dict[str, Any]:
"""Map histogram config to Superset form_data (viz_type histogram_v2).

Expand Down Expand Up @@ -2315,6 +2337,14 @@ def _bubble_chart_what(config: BubbleChartConfig) -> str:
return f"{config.entity.name}: {x_label} vs {y_label}"


def _heatmap_chart_what(config: HeatmapChartConfig) -> str:
"""Build the 'what' portion for a heatmap chart name."""
metric_label = (
config.metric.label or config.metric.name or config.metric.sql_expression
)
return f"{config.x_axis.name} vs {config.y_axis.name} by {metric_label}"


def _pivot_table_what(config: PivotTableChartConfig) -> str:
"""Build the 'what' portion for a pivot table chart name."""
# Pivot rows reject sql_expression at validation, so name is set.
Expand Down
3 changes: 3 additions & 0 deletions superset/mcp_service/chart/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from superset.mcp_service.chart.plugins.gantt import GanttChartPlugin
from superset.mcp_service.chart.plugins.gauge import GaugeChartPlugin
from superset.mcp_service.chart.plugins.handlebars import HandlebarsChartPlugin
from superset.mcp_service.chart.plugins.heatmap import HeatmapChartPlugin
from superset.mcp_service.chart.plugins.histogram import HistogramChartPlugin
from superset.mcp_service.chart.plugins.interactive_pivot import (
InteractivePivotChartPlugin,
Expand Down Expand Up @@ -62,6 +63,7 @@
register(BigNumberChartPlugin())
register(HistogramChartPlugin())
register(BoxPlotChartPlugin())
register(HeatmapChartPlugin())
register(WaterfallChartPlugin())
register(GanttChartPlugin())

Expand All @@ -72,6 +74,7 @@
"GanttChartPlugin",
"GaugeChartPlugin",
"HandlebarsChartPlugin",
"HeatmapChartPlugin",
"HistogramChartPlugin",
"InteractivePivotChartPlugin",
"MixedTimeseriesChartPlugin",
Expand Down
150 changes: 150 additions & 0 deletions superset/mcp_service/chart/plugins/heatmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Heatmap chart type plugin."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any, ClassVar

from superset.mcp_service.chart.chart_utils import (
_heatmap_chart_what,
_summarize_filters,
map_heatmap_config,
)
from superset.mcp_service.chart.plugin import BaseChartPlugin
from superset.mcp_service.chart.schemas import ColumnRef, HeatmapChartConfig
from superset.mcp_service.chart.validation.dataset_validator import DatasetValidator
from superset.mcp_service.common.error_schemas import ChartGenerationError


class HeatmapChartPlugin(BaseChartPlugin):
"""Plugin for heatmap chart type."""

chart_type = "heatmap_v2"
display_name = "Heatmap"
native_viz_types: ClassVar[Mapping[str, str]] = {
"heatmap_v2": "Heatmap",
}

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(exclude_unset=True)

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",
)
Comment on lines +45 to +150

@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

65 changes: 64 additions & 1 deletion superset/mcp_service/chart/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,67 @@ def record_implicit_metric_aggregate(self) -> "BubbleChartConfig":
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.

"""Config for heatmap charts (viz_type ``heatmap_v2``).

Matches the frontend Heatmap buildQuery contract: an ``x_axis`` column, a
single ``groupby`` column for the Y axis, and one ``metric`` colouring each
cell. ``normalize_across`` drives the server-side rank normalization
(whole heatmap, per-x, or per-y).
"""

model_config = ConfigDict(extra="ignore", populate_by_name=True)

chart_type: Literal["heatmap_v2"] = "heatmap_v2"
x_axis: ColumnRef = Field(
...,
description="Column along the X axis",
)
y_axis: ColumnRef = Field(
...,
description="Column along the Y axis (form_data 'groupby'; single-select)",
validation_alias=AliasChoices("y_axis", "groupby"),
)
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)",
)
Comment on lines +1751 to +1755

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

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.

"heatmap",
description="Range the cell colour is normalized against: the whole "
"'heatmap', each 'x' column, or each 'y' row (frontend default: "
"'heatmap'). Only takes effect when normalized=true.",
)
normalized: bool = Field(
False,
description="Colour cells by rank within 'normalize_across' rather than "
"the raw metric value. When false (the default) 'normalize_across' has "
"no visual effect.",
)
row_limit: int = Field(
10000, description="Max rows queried (cells = X × Y)", ge=1, le=100000
)
filters: List[FilterConfig] | None = Field(
None,
description="Structured filters (column/op/value). "
"Do NOT use adhoc_filters or raw SQL expressions.",
)

@model_validator(mode="after")
def reject_metric_style_dimensions(self) -> "HeatmapChartConfig":
"""x_axis and y_axis are dimensions, not metrics."""
for col, name in ((self.x_axis, "x_axis"), (self.y_axis, "y_axis")):
_reject_sql_expression_on_dimension(col, name)
if col and col.is_metric:
raise ValueError(
f"{name} must be a plain column, not a metric; drop "
"'aggregate'/'saved_metric' (metrics belong in the 'metric' "
"field)"
)
return self


class PivotTableChartConfig(BaseChartConfig):
model_config = ConfigDict(extra="ignore", populate_by_name=True)

Expand Down Expand Up @@ -3745,6 +3806,7 @@ def validate_gantt_roles(self) -> "GanttChartConfig":
| GaugeChartConfig
| TreemapChartConfig
| BubbleChartConfig
| HeatmapChartConfig
| PivotTableChartConfig
| InteractivePivotChartConfig
| MixedTimeseriesChartConfig
Expand All @@ -3758,7 +3820,8 @@ def validate_gantt_roles(self) -> "GanttChartConfig":
discriminator=CHART_TYPE_DISCRIMINATOR,
description=(
"Chart configuration - specify chart_type as 'xy', 'table', "
"'pie', 'gauge', 'treemap_v2', 'bubble_v2', 'pivot_table', "
"'pie', 'gauge', 'treemap_v2', 'bubble_v2', 'heatmap_v2', "
"'pivot_table', "
"'interactive_pivot', 'mixed_timeseries', 'handlebars', "
"'big_number', 'histogram', 'box_plot', 'waterfall', or 'gantt'"
),
Expand Down
Loading
Loading