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

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


The following commit(s) were added to refs/heads/master by this push:
     new fb276b08ddd fix(mcp): Skip misleading trend analysis for categorical 
ASCII charts (#39761)
fb276b08ddd is described below

commit fb276b08ddda9bcf9f4cdb358354b05339b0a3ff
Author: Alexandru Soare <[email protected]>
AuthorDate: Wed May 20 18:04:21 2026 +0300

    fix(mcp): Skip misleading trend analysis for categorical ASCII charts 
(#39761)
---
 superset/mcp_service/chart/ascii_charts.py         | 65 +++++++++++------
 .../mcp_service/chart/test_ascii_charts.py         | 83 +++++++++++++++++++++-
 2 files changed, 123 insertions(+), 25 deletions(-)

diff --git a/superset/mcp_service/chart/ascii_charts.py 
b/superset/mcp_service/chart/ascii_charts.py
index d8af0be3d6e..62107815f4e 100644
--- a/superset/mcp_service/chart/ascii_charts.py
+++ b/superset/mcp_service/chart/ascii_charts.py
@@ -251,7 +251,7 @@ def _generate_ascii_line_chart(data: list[Any], width: int, 
height: int) -> str:
     lines.append("═" * min(width, 60))
 
     # Extract values and labels for plotting
-    values, labels = _extract_time_series_data(data)
+    values, labels, is_temporal = _extract_time_series_data(data)
 
     if not values:
         return "No numeric data found for line chart"
@@ -265,7 +265,7 @@ def _generate_ascii_line_chart(data: list[Any], width: int, 
height: int) -> str:
         lines.extend(sparkline_data)
 
     # Add trend analysis
-    trend_analysis = _analyze_trend(values)
+    trend_analysis = _analyze_trend(values, is_temporal=is_temporal)
     lines.append("")
     lines.append("📊 Trend Analysis:")
     lines.extend(trend_analysis)
@@ -273,10 +273,17 @@ def _generate_ascii_line_chart(data: list[Any], width: 
int, height: int) -> str:
     return "\n".join(lines)
 
 
-def _extract_time_series_data(data: list[Any]) -> tuple[list[float], 
list[str]]:
-    """Extract time series data with labels."""
+def _extract_time_series_data(
+    data: list[Any],
+) -> tuple[list[float], list[str], bool]:
+    """Extract time series data with labels.
+
+    Returns a tuple of (values, labels, is_temporal) where is_temporal 
indicates
+    whether the label column appears to represent temporal data based on its 
name.
+    """
     values = []
     labels = []
+    is_temporal = False
 
     for row in data[:20]:  # Limit points for readability
         if isinstance(row, dict):
@@ -299,6 +306,7 @@ def _extract_time_series_data(data: list[Any]) -> 
tuple[list[float], list[str]]:
                         for date_word in ["date", "time", "month", "day", 
"year"]
                     ):
                         label_val = str(val)[:10]  # Truncate long dates
+                        is_temporal = True
                     else:
                         label_val = str(val)[:8]  # Truncate long strings
 
@@ -306,7 +314,7 @@ def _extract_time_series_data(data: list[Any]) -> 
tuple[list[float], list[str]]:
                 values.append(numeric_val)
                 labels.append(label_val or f"P{len(values)}")
 
-    return values, labels
+    return values, labels, is_temporal
 
 
 def _create_enhanced_line_chart(
@@ -407,8 +415,15 @@ def _draw_line_segment(
                     grid[y][x] = "│"
 
 
-def _analyze_trend(values: list[float]) -> list[str]:
-    """Analyze trend in the data."""
+def _analyze_trend(values: list[float], *, is_temporal: bool = True) -> 
list[str]:
+    """Analyze trend in the data.
+
+    Args:
+        values: Numeric data points to analyze.
+        is_temporal: Whether the data represents a time series. When False,
+            directional trend analysis is skipped because the ordering of
+            categorical data is arbitrary.
+    """
     if len(values) < 2:
         return ["• Insufficient data for trend analysis"]
 
@@ -421,24 +436,28 @@ def _analyze_trend(values: list[float]) -> list[str]:
     max_val = max(values)
     avg_val = sum(values) / len(values)
 
-    # Overall trend
-    if last_val > first_val * 1.1:
-        trend_icon = "📈"
-        trend_desc = "Strong upward trend"
-    elif last_val > first_val * 1.05:
-        trend_icon = "📊"
-        trend_desc = "Moderate upward trend"
-    elif last_val < first_val * 0.9:
-        trend_icon = "📉"
-        trend_desc = "Strong downward trend"
-    elif last_val < first_val * 0.95:
-        trend_icon = "📊"
-        trend_desc = "Moderate downward trend"
+    if is_temporal:
+        # Overall trend (only meaningful for temporally ordered data)
+        if last_val > first_val * 1.1:
+            trend_icon = "📈"
+            trend_desc = "Strong upward trend"
+        elif last_val > first_val * 1.05:
+            trend_icon = "📊"
+            trend_desc = "Moderate upward trend"
+        elif last_val < first_val * 0.9:
+            trend_icon = "📉"
+            trend_desc = "Strong downward trend"
+        elif last_val < first_val * 0.95:
+            trend_icon = "📊"
+            trend_desc = "Moderate downward trend"
+        else:
+            trend_icon = "➡️"
+            trend_desc = "Relatively stable"
+
+        analysis.append(f"• {trend_icon} {trend_desc}")
     else:
-        trend_icon = "➡️"
-        trend_desc = "Relatively stable"
+        analysis.append("• ⚠️ Data is categorical — directional trend not 
meaningful")
 
-    analysis.append(f"• {trend_icon} {trend_desc}")
     analysis.append(f"• Range: {min_val:.1f} to {max_val:.1f} (avg: 
{avg_val:.1f})")
 
     # Volatility
diff --git a/tests/unit_tests/mcp_service/chart/test_ascii_charts.py 
b/tests/unit_tests/mcp_service/chart/test_ascii_charts.py
index 45371f0f07a..65b9a9e116a 100644
--- a/tests/unit_tests/mcp_service/chart/test_ascii_charts.py
+++ b/tests/unit_tests/mcp_service/chart/test_ascii_charts.py
@@ -15,9 +15,88 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""Unit tests for ascii_charts.py NaN/null value handling."""
+"""Unit tests for ASCII charts: trend analysis, NaN/null handling,
+and boolean skipping."""
 
-from superset.mcp_service.chart.ascii_charts import generate_ascii_chart
+from superset.mcp_service.chart.ascii_charts import (
+    _analyze_trend,
+    _extract_time_series_data,
+    generate_ascii_chart,
+)
+
+
+def test_analyze_trend_temporal_upward():
+    values = [100.0, 110.0, 120.0, 130.0, 150.0]
+    result = _analyze_trend(values, is_temporal=True)
+    trend_line = result[0]
+    assert "upward trend" in trend_line.lower()
+    assert "categorical" not in trend_line.lower()
+
+
+def test_analyze_trend_temporal_downward():
+    values = [200.0, 180.0, 150.0, 120.0, 100.0]
+    result = _analyze_trend(values, is_temporal=True)
+    trend_line = result[0]
+    assert "downward trend" in trend_line.lower()
+
+
+def test_analyze_trend_temporal_stable():
+    values = [100.0, 101.0, 100.5, 100.2, 100.8]
+    result = _analyze_trend(values, is_temporal=True)
+    trend_line = result[0]
+    assert "stable" in trend_line.lower()
+
+
+def test_analyze_trend_categorical_skips_direction():
+    values = [200.0, 180.0, 150.0, 120.0, 100.0]
+    result = _analyze_trend(values, is_temporal=False)
+    trend_line = result[0]
+    assert "categorical" in trend_line.lower()
+    assert "downward" not in trend_line.lower()
+    assert "upward" not in trend_line.lower()
+
+
+def test_analyze_trend_categorical_keeps_range_and_volatility():
+    values = [200.0, 180.0, 150.0, 120.0, 100.0]
+    result = _analyze_trend(values, is_temporal=False)
+    full_text = "\n".join(result)
+    assert "Range:" in full_text
+    assert "Volatility:" in full_text
+
+
+def test_analyze_trend_defaults_to_temporal():
+    values = [100.0, 150.0, 200.0]
+    result = _analyze_trend(values)
+    trend_line = result[0]
+    assert "categorical" not in trend_line.lower()
+    assert "upward trend" in trend_line.lower()
+
+
+def test_extract_time_series_data_temporal_column():
+    data = [
+        {"date": "2024-01-01", "value": 100},
+        {"date": "2024-02-01", "value": 200},
+    ]
+    values, labels, is_temporal = _extract_time_series_data(data)
+    assert values == [100, 200]
+    assert is_temporal is True
+
+
+def test_extract_time_series_data_categorical_column():
+    data = [
+        {"country": "Argentina", "value": 500},
+        {"country": "Brazil", "value": 300},
+    ]
+    values, labels, is_temporal = _extract_time_series_data(data)
+    assert values == [500, 300]
+    assert is_temporal is False
+
+
+def test_extract_time_series_data_time_keyword_variants():
+    for key in ["timestamp", "created_time", "month_name", "day_of_week", 
"year_end"]:
+        data = [{key: "val", "metric": 42}]
+        _, _, is_temporal = _extract_time_series_data(data)
+        assert is_temporal is True, f"Expected temporal for column '{key}'"
 
 
 def test_bar_chart_with_null_values_does_not_raise() -> None:

Reply via email to