gkneighb commented on code in PR #41860:
URL: https://github.com/apache/superset/pull/41860#discussion_r3575724731


##########
superset/mcp_service/chart/plugins/box_plot.py:
##########
@@ -0,0 +1,155 @@
+# 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.
+
+"""Box plot 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 (
+    _summarize_filters,
+    map_box_plot_config,
+)
+from superset.mcp_service.chart.plugin import BaseChartPlugin
+from superset.mcp_service.chart.schemas import BoxPlotChartConfig, ColumnRef
+from superset.mcp_service.chart.validation.dataset_validator import 
DatasetValidator
+from superset.mcp_service.common.error_schemas import ChartGenerationError
+
+
+class BoxPlotChartPlugin(BaseChartPlugin):
+    """Plugin for box plot chart type."""
+
+    chart_type = "box_plot"
+    display_name = "Box Plot"
+    native_viz_types: ClassVar[Mapping[str, str]] = {
+        "box_plot": "Box Plot",
+    }
+
+    def pre_validate(
+        self,
+        config: dict[str, Any],
+    ) -> ChartGenerationError | None:
+        missing_fields = []
+        if "metrics" not in config:
+            missing_fields.append("'metrics' (values whose spread is plotted)")
+        if "distribute_across" not in config:
+            missing_fields.append(
+                "'distribute_across' (columns whose values form the samples "
+                "inside each box, e.g. a temporal column)"
+            )

Review Comment:
   Fixed in the latest push — pre_validate accepts either distribute_across or 
its columns alias, with a test.



##########
superset/mcp_service/chart/schemas.py:
##########
@@ -1846,6 +1847,164 @@ def validate_unique_column_labels(self) -> 
"XYChartConfig":
         return self
 
 
+class HistogramChartConfig(UnknownFieldCheckMixin):
+    """Config for histogram charts (viz_type ``histogram_v2``)."""
+
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    chart_type: Literal["histogram"] = "histogram"
+    column: ColumnRef = Field(
+        ...,
+        description="Numeric column to bin (a physical dataset column)",
+    )
+    groupby: List[ColumnRef] | None = Field(
+        None,
+        description="Optional dimensions to split the distribution into 
series",
+    )
+    bins: int = Field(5, description="Number of histogram bins", ge=1, le=1000)
+    normalize: bool = Field(False, description="Normalize bin counts to 
proportions")
+    cumulative: bool = Field(False, description="Accumulate bin counts left to 
right")
+    filters: List[FilterConfig] | None = Field(
+        None,
+        description="Structured filters (column/op/value). "
+        "Do NOT use adhoc_filters or raw SQL expressions.",
+    )
+    row_limit: int = Field(10000, description="Max rows sampled", ge=1, 
le=100000)
+
+    @model_validator(mode="after")
+    def reject_metric_style_column(self) -> "HistogramChartConfig":
+        """The binned column is a physical column, not a metric."""
+        _reject_sql_expression_on_dimension(self.column, "column")
+        if self.column and self.column.saved_metric:
+            raise ValueError(
+                "column cannot use saved_metric=True; histograms bin a "
+                "physical numeric column"
+            )
+        for i, col in enumerate(self.groupby or []):
+            _reject_sql_expression_on_dimension(col, f"groupby[{i}]")
+            if col.saved_metric:
+                raise ValueError(
+                    f"groupby[{i}] cannot use saved_metric=True; "
+                    "saved metrics are not dimensions"
+                )
+        return self
+
+
+class BoxPlotChartConfig(UnknownFieldCheckMixin):
+    """Config for box plot charts (viz_type ``box_plot``)."""
+
+    model_config = ConfigDict(extra="ignore", populate_by_name=True)
+
+    chart_type: Literal["box_plot"] = "box_plot"
+    metrics: List[ColumnRef] = Field(
+        ...,
+        min_length=1,
+        description="Metrics whose distributions are plotted (use aggregate "
+        "e.g. AVG, SUM for ad-hoc, or saved_metric=True for saved metrics)",
+    )
+    distribute_across: List[ColumnRef] = Field(
+        ...,
+        min_length=1,
+        validation_alias=AliasChoices("distribute_across", "columns"),
+        description="Columns whose distinct values form the SAMPLES inside "
+        "each box (typically a temporal column such as month) — the "
+        "distribution is computed across these values; maps to the "
+        "frontend's 'Distribute across' control (form_data 'columns'). "
+        "This does NOT split boxes; use 'dimensions' for that.",
+    )
+    dimensions: List[ColumnRef] | None = Field(
+        None,
+        validation_alias=AliasChoices("dimensions", "groupby"),
+        description="Columns whose values split the chart into boxes — one "
+        "box per value on the x-axis (form_data 'groupby'). Omit for a "
+        "single box showing each metric's overall distribution.",
+    )
+    whisker_type: Literal["tukey", "min_max", "percentile"] = Field(
+        "tukey",
+        description="Whisker algorithm: 'tukey' (1.5 IQR), 'min_max' (no "
+        "outliers), or 'percentile' (requires percentile_low/percentile_high)",
+    )
+    percentile_low: int | None = Field(
+        None, description="Lower whisker percentile (0-100)", ge=0, le=100
+    )
+    percentile_high: int | None = Field(
+        None, description="Upper whisker percentile (0-100)", ge=0, le=100
+    )
+    filters: List[FilterConfig] | None = Field(
+        None,
+        description="Structured filters (column/op/value). "
+        "Do NOT use adhoc_filters or raw SQL expressions.",
+    )
+    row_limit: int = Field(
+        10000,
+        description="Max grouped rows (frontend shared default)",
+        ge=1,
+        le=50000,
+    )
+    number_format: str = Field("SMART_NUMBER", max_length=50)
+    date_format: str = Field("smart_date", max_length=50)
+
+    @model_validator(mode="before")
+    @classmethod
+    def accept_frontend_whisker_options(cls, data: Any) -> Any:
+        """Translate the frontend's whiskerOptions strings ('Tukey',
+        'Min/max (no outliers)', '<low>/<high> percentiles') so configs
+        copied from existing Superset form_data are accepted rather than
+        refused."""
+        if (
+            isinstance(data, dict)
+            and "whiskerOptions" in data
+            and "whisker_type" not in data
+        ):

Review Comment:
   Fixed in the latest push — whiskerOptions is always consumed; an explicit 
whisker_type takes precedence, so sending both keys no longer trips the 
unknown-field check. With a test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to