diff --git a/.gitignore b/.gitignore
index 6f1b2261d8a..18f614dbcc2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -137,6 +137,7 @@ PROJECT.md
.aider*
.claude_rc*
.claude/settings.local.json
+.claude/worktrees/
.env.local
oxc-custom-build/
*.code-workspace
diff --git a/SCOPE.md b/SCOPE.md
index 0aef0d10e68..434f133b006 100644
--- a/SCOPE.md
+++ b/SCOPE.md
@@ -62,9 +62,9 @@ unchanged. "Retarget" = migrate saved charts to an existing modern chart via a
## Phases / sub-PRs (each targets this feature branch)
### Phase 0 — Dead code deletion
-- [ ] Delete `superset-frontend/plugins/legacy-preset-chart-deckgl/` (empty husk: only a stale `node_modules/`, unreferenced, absent from `package.json`)
-- [ ] Delete `legacy-preset-chart-nvd3/src/BoxPlot/` leftovers (unregistered; superseded by ECharts BoxPlot)
-- [ ] Delete orphaned `viz.py` classes with no registered frontend: `MapboxViz`, `MapLibreViz`, `EventFlowViz` (+ their tests)
+- [x] `superset-frontend/plugins/legacy-preset-chart-deckgl/` turned out to have **zero tracked files** — it was a stale local `node_modules/` husk, removed locally, nothing to commit
+- [x] Delete `legacy-preset-chart-nvd3/src/BoxPlot/` leftovers (unregistered; superseded by ECharts BoxPlot)
+- [x] Delete orphaned `viz.py` classes with no registered frontend: `MapboxViz`, `MapLibreViz`, `EventFlowViz` (no tests referenced them; `mapbox` saved charts were already migrated to `point_cluster_map` by revision `ce6bd21901ab`; the frontend `point_cluster_map` plugin has its own modern `buildQuery`, so removing `MapLibreViz` also fixes cache warm-up, which routed any `viz_type in viz_types` through the legacy path)
### Phase 1 — Chart migrations (tiers 1→4, easiest first; order above)
Per-chart checklist:
@@ -92,6 +92,10 @@ Per-chart checklist:
(append entries as sub-PRs merge: date, sub-PR #, summary)
+- 2026-07-02 — Phase 0: removed orphaned `viz.py` classes (`MapboxViz`, `MapLibreViz`,
+ `EventFlowViz`, −204 lines) and the unregistered nvd3 BoxPlot story leftovers.
+ Verified: `ruff` clean, `tests/unit_tests/test_viz_*` pass.
+
## Decisions
- **compare** → migrating to `echarts_timeseries_line`; nvd3's interactive % rebasing is not preserved (flagged in UPDATES.md).
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/NVD3BoxPlot.stories.tsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/NVD3BoxPlot.stories.tsx
deleted file mode 100644
index acf4317cca8..00000000000
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/NVD3BoxPlot.stories.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.
- */
-
-import { SuperChart, VizType } from '@superset-ui/core';
-import { EchartsBoxPlotChartPlugin } from '@superset-ui/plugin-chart-echarts';
-import { dummyDatasource, withResizableChartDemo } from '@storybook-shared';
-import data from './data';
-
-new EchartsBoxPlotChartPlugin().configure({ key: 'box-plot' }).register();
-
-export default {
- title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/BoxPlot',
- decorators: [withResizableChartDemo],
- args: {
- colorScheme: 'd3Category10',
- whiskerOptions: 'Min/max (no outliers)',
- },
- argTypes: {
- colorScheme: {
- control: 'select',
- options: [
- 'supersetColors',
- 'd3Category10',
- 'bnbColors',
- 'googleCategory20c',
- ],
- },
- whiskerOptions: {
- control: 'select',
- options: [
- 'Tukey',
- 'Min/max (no outliers)',
- '2/98 percentiles',
- '9/91 percentiles',
- ],
- },
- },
-};
-
-export const Basic = ({
- colorScheme,
- whiskerOptions,
- width,
- height,
-}: {
- colorScheme: string;
- whiskerOptions: string;
- width: number;
- height: number;
-}) => (
-
-);
diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/data.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/data.ts
deleted file mode 100644
index 02837a59c3e..00000000000
--- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/data.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.
- */
-
-/* eslint-disable sort-keys, no-magic-numbers */
-// Data format for ECharts BoxPlot - requires metric__min, metric__q1, metric__median, etc.
-export default [
- {
- region: 'East Asia & Pacific',
- sum__SP_POP_TOTL__min: 1031863394.0,
- sum__SP_POP_TOTL__q1: 1384725172.5,
- sum__SP_POP_TOTL__median: 1717904169.0,
- sum__SP_POP_TOTL__q3: 2032724922.5,
- sum__SP_POP_TOTL__max: 2240687901.0,
- sum__SP_POP_TOTL__mean: 1681000000.0,
- sum__SP_POP_TOTL__count: 50,
- sum__SP_POP_TOTL__outliers: [],
- },
- {
- region: 'Europe & Central Asia',
- sum__SP_POP_TOTL__min: 660881033.0,
- sum__SP_POP_TOTL__q1: 751386460.5,
- sum__SP_POP_TOTL__median: 820716895.0,
- sum__SP_POP_TOTL__q3: 862814192.5,
- sum__SP_POP_TOTL__max: 903095786.0,
- sum__SP_POP_TOTL__mean: 799778873.0,
- sum__SP_POP_TOTL__count: 50,
- sum__SP_POP_TOTL__outliers: [],
- },
- {
- region: 'Latin America & Caribbean',
- sum__SP_POP_TOTL__min: 220564224.0,
- sum__SP_POP_TOTL__q1: 313690832.5,
- sum__SP_POP_TOTL__median: 421490233.0,
- sum__SP_POP_TOTL__q3: 529668114.5,
- sum__SP_POP_TOTL__max: 626270167.0,
- sum__SP_POP_TOTL__mean: 422336714.0,
- sum__SP_POP_TOTL__count: 50,
- sum__SP_POP_TOTL__outliers: [],
- },
- {
- region: 'South Asia',
- sum__SP_POP_TOTL__min: 572036107.0,
- sum__SP_POP_TOTL__q1: 772373036.5,
- sum__SP_POP_TOTL__median: 1059570231.0,
- sum__SP_POP_TOTL__q3: 1398841234.0,
- sum__SP_POP_TOTL__max: 1720976995.0,
- sum__SP_POP_TOTL__mean: 1104759521.0,
- sum__SP_POP_TOTL__count: 50,
- sum__SP_POP_TOTL__outliers: [],
- },
- {
- region: 'Sub-Saharan Africa',
- sum__SP_POP_TOTL__min: 228268752.0,
- sum__SP_POP_TOTL__q1: 320037758.0,
- sum__SP_POP_TOTL__median: 467337821.0,
- sum__SP_POP_TOTL__q3: 676768689.0,
- sum__SP_POP_TOTL__max: 974315323.0,
- sum__SP_POP_TOTL__mean: 533345669.0,
- sum__SP_POP_TOTL__count: 50,
- sum__SP_POP_TOTL__outliers: [1100000000, 1200000000],
- },
-];
diff --git a/superset/viz.py b/superset/viz.py
index 412436854f7..d04286f0359 100644
--- a/superset/viz.py
+++ b/superset/viz.py
@@ -1446,177 +1446,6 @@ class HorizonViz(NVD3TimeSeriesViz):
)
-class MapboxViz(BaseViz):
- """Rich maps made with Mapbox"""
-
- viz_type = "mapbox"
- verbose_name = _("Mapbox")
- is_timeseries = False
- credits = "Mapbox GL JS"
-
- @deprecated(deprecated_in="3.0")
- def query_obj(self) -> QueryObjectDict:
- query_obj = super().query_obj()
- label_col = self.form_data.get("mapbox_label")
-
- if not self.form_data.get("groupby"):
- if (
- self.form_data.get("all_columns_x") is None
- or self.form_data.get("all_columns_y") is None
- ):
- raise QueryObjectValidationError(
- _("[Longitude] and [Latitude] must be set")
- )
- query_obj["columns"] = [
- self.form_data.get("all_columns_x"),
- self.form_data.get("all_columns_y"),
- ]
-
- if label_col and len(label_col) >= 1:
- if label_col[0] == "count":
- raise QueryObjectValidationError(
- _(
- "Must have a [Group By] column to have 'count' as the "
- + "[Label]"
- )
- )
- query_obj["columns"].append(label_col[0])
-
- if self.form_data.get("point_radius") != "Auto":
- query_obj["columns"].append(self.form_data.get("point_radius"))
-
- # Ensure this value is sorted so that it does not
- # cause the cache key generation (which hashes the
- # query object) to generate different keys for values
- # that should be considered the same.
- query_obj["columns"] = sorted(set(query_obj["columns"]))
- else:
- # Ensuring columns chosen are all in group by
- if (
- label_col
- and len(label_col) >= 1
- and label_col[0] != "count"
- and label_col[0] not in self.form_data["groupby"]
- ):
- raise QueryObjectValidationError(
- _("Choice of [Label] must be present in [Group By]")
- )
-
- if (
- self.form_data.get("point_radius") != "Auto"
- and self.form_data.get("point_radius") not in self.form_data["groupby"]
- ):
- raise QueryObjectValidationError(
- _("Choice of [Point Radius] must be present in [Group By]")
- )
-
- if (
- self.form_data.get("all_columns_x") not in self.form_data["groupby"]
- or self.form_data.get("all_columns_y") not in self.form_data["groupby"]
- ):
- raise QueryObjectValidationError(
- _(
- "[Longitude] and [Latitude] columns must be present in "
- + "[Group By]"
- )
- )
- return query_obj
-
- @deprecated(deprecated_in="3.0")
- def get_data(self, df: pd.DataFrame) -> VizData:
- if df.empty:
- return None
-
- label_col = self.form_data.get("mapbox_label")
- has_custom_metric = label_col is not None and len(label_col) > 0
- metric_col = [None] * len(df.index)
- if has_custom_metric:
- if label_col[0] == self.form_data.get("all_columns_x"): # type: ignore
- metric_col = df[self.form_data.get("all_columns_x")]
- elif label_col[0] == self.form_data.get("all_columns_y"): # type: ignore
- metric_col = df[self.form_data.get("all_columns_y")]
- else:
- metric_col = df[label_col[0]] # type: ignore
- point_radius_col = (
- [None] * len(df.index)
- if self.form_data.get("point_radius") == "Auto"
- else df[self.form_data.get("point_radius")]
- )
-
- # limiting geo precision as long decimal values trigger issues
- # around json-bignumber in Mapbox
- geo_precision = 10
- # using geoJSON formatting
- geo_json = {
- "type": "FeatureCollection",
- "features": [
- {
- "type": "Feature",
- "properties": {"metric": metric, "radius": point_radius},
- "geometry": {
- "type": "Point",
- "coordinates": [
- round(lon, geo_precision),
- round(lat, geo_precision),
- ],
- },
- }
- for lon, lat, metric, point_radius in zip(
- df[self.form_data.get("all_columns_x")],
- df[self.form_data.get("all_columns_y")],
- metric_col,
- point_radius_col,
- strict=False,
- )
- ],
- }
-
- x_series, y_series = (
- df[self.form_data.get("all_columns_x")],
- df[self.form_data.get("all_columns_y")],
- )
- south_west = [x_series.min(), y_series.min()]
- north_east = [x_series.max(), y_series.max()]
-
- return {
- "geoJSON": geo_json,
- "hasCustomMetric": has_custom_metric,
- "mapboxApiKey": current_app.config["MAPBOX_API_KEY"],
- "mapStyle": self.form_data.get("mapbox_style"),
- "aggregatorName": self.form_data.get("pandas_aggfunc"),
- "clusteringRadius": self.form_data.get("clustering_radius"),
- "pointRadiusUnit": self.form_data.get("point_radius_unit"),
- "globalOpacity": self.form_data.get("global_opacity"),
- "bounds": [south_west, north_east],
- "renderWhileDragging": self.form_data.get("render_while_dragging"),
- "tooltip": self.form_data.get("rich_tooltip"),
- "color": self.form_data.get("mapbox_color"),
- }
-
-
-class MapLibreViz(MapboxViz):
- """Rich maps made with MapLibre"""
-
- viz_type = "point_cluster_map"
- verbose_name = _("Point Cluster Map")
- credits = 'MapLibre GL JS'
-
- @deprecated(deprecated_in="3.0")
- def query_obj(self) -> QueryObjectDict:
- self.form_data["mapbox_label"] = self.form_data.get("map_label")
- return super().query_obj()
-
- @deprecated(deprecated_in="3.0")
- def get_data(self, df: pd.DataFrame) -> VizData:
- self.form_data["mapbox_label"] = self.form_data.get("map_label")
- self.form_data["mapbox_style"] = self.form_data.get("map_style")
- self.form_data["mapbox_color"] = self.form_data.get("map_color")
- data = super().get_data(df)
- if data:
- data.pop("mapboxApiKey", None)
- return data
-
-
class DeckGLMultiLayer(BaseViz):
"""Pile on multiple DeckGL layers"""
@@ -2657,39 +2486,6 @@ class DeckArc(BaseDeckGLViz):
}
-class EventFlowViz(BaseViz):
- """A visualization to explore patterns in event sequences"""
-
- viz_type = "event_flow"
- verbose_name = _("Event flow")
- credits = 'from @data-ui'
- is_timeseries = True
-
- @deprecated(deprecated_in="3.0")
- def query_obj(self) -> QueryObjectDict:
- query = super().query_obj()
- form_data = self.form_data
-
- event_key = form_data["all_columns_x"]
- entity_key = form_data["entity"]
- meta_keys = [
- col
- for col in form_data["all_columns"] or []
- if col not in (event_key, entity_key)
- ]
-
- query["columns"] = [event_key, entity_key] + meta_keys
-
- if form_data["order_by_entity"]:
- query["orderby"] = [(entity_key, True)]
-
- return query
-
- @deprecated(deprecated_in="3.0")
- def get_data(self, df: pd.DataFrame) -> VizData:
- return df.to_dict(orient="records")
-
-
class PairedTTestViz(BaseViz):
"""A table displaying paired t-test values"""