From 5d86215a9d4a4cab483d44e2ba27082cb496f036 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 2 Jul 2026 21:15:51 -0700 Subject: [PATCH 01/84] chore(viz): add SCOPE/UPDATES trackers for legacy viz pipeline removal Working documents for the remove-legacy-viz-pipeline feature branch; stripped before final merge. Co-Authored-By: Claude Fable 5 --- SCOPE.md | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++ UPDATES.md | 48 +++++++++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 SCOPE.md create mode 100644 UPDATES.md diff --git a/SCOPE.md b/SCOPE.md new file mode 100644 index 000000000000..0aef0d10e68e --- /dev/null +++ b/SCOPE.md @@ -0,0 +1,103 @@ + + +# SCOPE: Remove the legacy viz pipeline (`explore_json` + `viz.py`) + +> **This file is a working tracker for the `remove-legacy-viz-pipeline` feature branch.** +> It accumulates everything done on this branch and will be distilled into the final +> PR body. It is deleted before the feature branch merges to `master`. +> Companion file: [UPDATES.md](UPDATES.md) staging entries destined for `UPDATING.md`. + +## Goal + +Migrate every remaining `useLegacyApi: true` viz plugin to the v1 `/api/v1/chart/data` +endpoint (adding `buildQuery.ts` + `post_processing` operations / `transformProps` +reshaping as needed), then delete the deprecated `explore_json` endpoints and +`superset/viz.py` entirely, and drop the `legacy-` prefix from the surviving plugin +package names. Both endpoints have carried `@deprecated(eol_version="5.0.0")` gates; +this branch is the EOL. + +## Inventory: the 15 remaining legacy viz types + +| # | viz_type | Plugin | Strategy | Status | +|---|----------|--------|----------|--------| +| 1 | `para` | legacy-plugin-chart-parallel-coordinates | Convert in place (pass-through) | ⬜ | +| 2 | `country_map` | legacy-plugin-chart-country-map | Convert in place (select/rename) | ⬜ | +| 3 | `bullet` | legacy-preset-chart-nvd3/Bullet | Convert in place (single metric) | ⬜ | +| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | ⬜ | +| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | ⬜ | +| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | ⬜ | +| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | ⬜ | +| 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ⬜ | +| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ⬜ | +| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ⬜ | +| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | ⬜ | +| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | ⬜ | +| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | ⬜ | +| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | ⬜ | +| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | ⬜ | + +"Convert in place" = keep the renderer and the `viz_type` key, add a modern +`buildQuery.ts`, move the `viz.py` `get_data()` reshape into `transformProps.ts`, +flip `useLegacyApi` off. No saved-chart DB migration needed since `viz_type` is +unchanged. "Retarget" = migrate saved charts to an existing modern chart via a +`MigrateViz` processor + Alembic revision, then delete the legacy plugin. + +## 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) + +### Phase 1 — Chart migrations (tiers 1→4, easiest first; order above) +Per-chart checklist: +- [ ] `buildQuery.ts` returning a `buildQueryContext(...)` with needed `post_processing` operators +- [ ] `transformProps.ts` ports the `viz.py` `get_data()` reshape (where not expressible as post_processing) +- [ ] `useLegacyApi` removed from plugin metadata +- [ ] Jest tests: buildQuery output + transformProps against fixtures captured from the legacy pipeline +- [ ] Verify legacy saved-chart `form_data` (`granularity_sqla`/`time_range`) flows through `buildQueryContext` — add a normalizing same-viz-type `MigrateViz` if not +- [ ] For retargets: `MigrateViz` processor + `superset/cli/viz_migrations.py` registration + Alembic revision (with MySQL MEDIUMTEXT widening) + migration tests +- [ ] UPDATES.md entry if user-facing behavior changes + +### Phase 2 — Backend removal +- [ ] Remove `explore_json` / `explore_json_data` / `generate_json` from `superset/views/core.py` (incl. CSV/XLSX/async branches) +- [ ] Remove CSRF exemption (`superset/config.py`) and GLOBAL_ASYNC_QUERIES explore_json path +- [ ] Delete `superset/viz.py`; unwind importers: `views/utils.py` (`get_viz`), `models/slice.py` (`Slice.viz`), `commands/chart/warm_up_cache.py`, `common/query_context_processor.py` (annotation path), `security/manager.py` (`BaseViz` overload) +- [ ] Delete `tests/unit_tests/test_viz_*.py`, `tests/integration_tests/viz_tests.py` +- [ ] Frontend plumbing: remove `useLegacyApi` from `ChartMetadata`, `shouldUseLegacyApi`, `/explore_json/` branches in `exploreUtils`, `chartAction.ts`, `ChartClient.ts`, `StatefulChart.tsx`, `DrillByModal.tsx` + +### Phase 3 — Renames (folded into per-chart PRs where possible) +- [ ] `legacy-plugin-chart-*` → `plugin-chart-*` (dirs, npm names, `file:` deps, MainPreset imports, Storybook/docs) +- [ ] Resolve what remains of `legacy-preset-chart-nvd3` (bullet, time_pivot survivors) +- [ ] Drop "(legacy)" suffixes from user-facing chart names + +## Log of work completed + +(append entries as sub-PRs merge: date, sub-PR #, summary) + +## Decisions + +- **compare** → migrating to `echarts_timeseries_line`; nvd3's interactive % rebasing is not preserved (flagged in UPDATES.md). +- **partition** → attempting convert-in-place; if the transformProps port proves unmaintainable, fall back to proposing deprecation on dev@. +- Feature branch lives on `apache/superset` so phase PRs can target it. Tracking files (SCOPE.md, UPDATES.md) are stripped before final merge. + +## Open questions + +(record anything needing Evan's / community input) diff --git a/UPDATES.md b/UPDATES.md new file mode 100644 index 000000000000..127ed1ab06e5 --- /dev/null +++ b/UPDATES.md @@ -0,0 +1,48 @@ + + +# UPDATES: staged entries for UPDATING.md + +> **Working tracker for the `remove-legacy-viz-pipeline` feature branch.** +> Each bullet below is drafted in UPDATING.md's voice and will be moved into +> `UPDATING.md` under "Next" before the feature branch merges. This file is +> then deleted. + +## Breaking changes (drafts) + +- [ ] The deprecated `/superset/explore_json/` endpoints (deprecated since 3.0, + EOL 5.0.0) have been removed. All charts now use `/api/v1/chart/data`. +- [ ] `superset/viz.py` has been removed. Anything importing `superset.viz` + (custom code, plugins) must migrate to the QueryContext / + `pandas_postprocessing` pipeline. +- [ ] The `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` has been + removed. Third-party viz plugins that set it must provide a `buildQuery` + and use the v1 chart data API. +- [ ] Legacy plugin packages have been renamed to drop the `legacy-` prefix + (e.g. `@superset-ui/legacy-plugin-chart-chord` → `@superset-ui/plugin-chart-chord`). + (enumerate final list here) +- [ ] "Bubble Chart (legacy)" (`bubble`) charts are automatically migrated to the + ECharts Bubble Chart (`bubble_v2`). +- [ ] "Time-series Percent Change" (`compare`) charts are automatically migrated to + the ECharts Line Chart. The interactive re-basing of the nvd3 renderer is not + preserved. + +## Non-breaking notes (drafts) + +(add as they come up — e.g. behavior nuances found while porting get_data reshapes) From b1bc8fa8f2157ee3baf4c45fe2463ae2dea05fc1 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 21:50:57 -0700 Subject: [PATCH 02/84] =?UTF-8?q?chore(viz):=20phase=200=20=E2=80=94=20rem?= =?UTF-8?q?ove=20orphaned=20viz.py=20classes=20and=20nvd3=20BoxPlot=20left?= =?UTF-8?q?overs=20(#41715)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Claude Code --- .gitignore | 1 + SCOPE.md | 10 +- .../BoxPlot/stories/NVD3BoxPlot.stories.tsx | 81 ------- .../src/BoxPlot/stories/data.ts | 78 ------- superset/viz.py | 204 ------------------ 5 files changed, 8 insertions(+), 366 deletions(-) delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/NVD3BoxPlot.stories.tsx delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/BoxPlot/stories/data.ts diff --git a/.gitignore b/.gitignore index 2c721332df71..94b6b7c0f7a2 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,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 0aef0d10e68e..434f133b0067 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 acf4317cca8e..000000000000 --- 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 02837a59c3e0..000000000000 --- 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 8ee02d842401..699706785d4b 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1436,177 +1436,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""" @@ -2647,39 +2476,6 @@ def get_data(self, df: pd.DataFrame) -> VizData: } -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""" From f8dd65cc0b98e9079a59d4e4d4de04b5e8edd144 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:17:02 -0700 Subject: [PATCH 03/84] feat(country-map): migrate country_map chart to v1 chart data API (#41717) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 34 ++++++++++++ .../src/index.ts | 2 +- .../src/transformProps.ts | 24 +++++++-- .../test/buildQuery.test.ts | 52 ++++++++++++++++++ .../test/transformProps.test.ts | 53 +++++++++++++++++++ .../test/tsconfig.json | 8 +++ 6 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts new file mode 100644 index 000000000000..6caa2cb6f2cb --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts @@ -0,0 +1,34 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * Mirrors the legacy CountryMapViz.query_obj: one query grouped by the + * ISO-code entity column, selecting the single metric. + */ +export default function buildQuery(formData: QueryFormData) { + const { entity, metric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: entity ? [entity] : [], + metrics: metric ? [metric] : [], + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts index 8da3e8e6ae3d..dc402fa85455 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts @@ -48,7 +48,6 @@ const metadata = new ChartMetadata({ ], thumbnail, thumbnailDark, - useLegacyApi: true, behaviors: [ Behavior.InteractiveChart, Behavior.DrillToDetail, @@ -60,6 +59,7 @@ export default class CountryMapChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactCountryMap'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts index f4ee4728d1fe..8267765eb276 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts @@ -16,7 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, getValueFormatter } from '@superset-ui/core'; +import { + ChartProps, + getColumnLabel, + getMetricLabel, + getValueFormatter, +} from '@superset-ui/core'; export default function transformProps(chartProps: ChartProps) { const { @@ -45,7 +50,20 @@ export default function transformProps(chartProps: ChartProps) { columnFormats = {}, currencyCodeColumn, } = datasource; - const { data, detected_currency: detectedCurrency } = queriesData[0]; + const { data: rawData, detected_currency: detectedCurrency } = queriesData[0]; + + // The legacy explore_json endpoint renamed the entity and metric columns + // server-side; the v1 chart data endpoint returns them under their own + // labels, so the rename happens here. + const entityLabel = getColumnLabel(entity); + const metricLabel = getMetricLabel(metric); + // rename only rows carrying both source labels, so pre-shaped legacy + // payloads pass through even when the entity column is named country_id + const data = (rawData ?? []).map((row: Record) => + entityLabel in row && metricLabel in row + ? { country_id: row[entityLabel], metric: row[metricLabel] } + : row, + ); const formatter = getValueFormatter( metric, @@ -64,7 +82,7 @@ export default function transformProps(chartProps: ChartProps) { return { width, height, - data: queriesData[0].data, + data, country: selectCountry ? String(selectCountry).toLowerCase() : null, linearColorScheme, numberFormat, // left for backward compatibility diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts new file mode 100644 index 000000000000..df0dff660748 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts @@ -0,0 +1,52 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: 'country_map', + select_country: 'france', + entity: 'code_insee', + metric: 'avg__num', +}; + +test('groups by the ISO entity column and selects the single metric', () => { + const [query] = buildQuery(formData).queries; + expect(query.columns).toEqual(['code_insee']); + expect(query.metrics).toEqual(['avg__num']); +}); + +test('maps legacy granularity_sqla saved form data', () => { + const [query] = buildQuery(formData).queries; + expect(query.granularity).toEqual('ds'); +}); + +test('supports adhoc metrics', () => { + const adhocMetric = { + expressionType: 'SIMPLE' as const, + aggregate: 'AVG' as const, + column: { column_name: 'num' }, + label: 'AVG(num)', + }; + const [query] = buildQuery({ ...formData, metric: adhocMetric }).queries; + expect(query.metrics).toEqual([adhocMetric]); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts index 2ff9a593922c..18fd84267360 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts @@ -74,3 +74,56 @@ test('defaults hooks to an empty object when none are provided', () => { expect(transformed.onContextMenu).toBeUndefined(); expect(transformed.setDataMask).toBeUndefined(); }); + +test('renames v1 API records to the country_id/metric shape', () => { + const transformed = transformProps( + createProps( + {}, + { + queriesData: [ + { + data: [ + { country_code: 'FRA', count: 10 }, + { country_code: 'DEU', count: 7 }, + ], + }, + ], + }, + ), + ); + expect(transformed.data).toEqual([ + { country_id: 'FRA', metric: 10 }, + { country_id: 'DEU', metric: 7 }, + ]); +}); + +test('renames v1 records even when the entity column is named country_id', () => { + const transformed = transformProps( + createProps( + { entity: 'country_id' }, + { + queriesData: [{ data: [{ country_id: 'FRA', count: 10 }] }], + }, + ), + ); + expect(transformed.data).toEqual([{ country_id: 'FRA', metric: 10 }]); +}); + +test('renames v1 API records using adhoc metric labels', () => { + const transformed = transformProps( + createProps( + { + metric: { + expressionType: 'SIMPLE', + aggregate: 'AVG', + column: { column_name: 'num' }, + label: 'AVG(num)', + }, + }, + { + queriesData: [{ data: [{ country_code: 'FRA', 'AVG(num)': 3.14 }] }], + }, + ), + ); + expect(transformed.data).toEqual([{ country_id: 'FRA', metric: 3.14 }]); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json new file mode 100644 index 000000000000..be84a09330ca --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false + }, + "extends": "../../../tsconfig.json", + "include": ["**/*", "../types/**/*", "../../../types/**/*"] +} From a4bcd2cc874ab389ba8c14e520cf6db7268b39f5 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:20:12 -0700 Subject: [PATCH 04/84] feat(parallel-coordinates): migrate para chart to v1 chart data API (#41716) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 62 +++++++++++ .../src/index.ts | 4 +- .../test/buildQuery.test.ts | 101 ++++++++++++++++++ .../test/tsconfig.json | 8 ++ 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts new file mode 100644 index 000000000000..66d13206966f --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts @@ -0,0 +1,62 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + getMetricLabel, + QueryFormData, + QueryFormMetric, + QueryFormOrderBy, +} from '@superset-ui/core'; + +/** + * Mirrors the query the legacy `para` viz built server-side: one query + * grouped by `series` selecting all metrics (including the secondary + * "color" metric, aliased into `metrics` by extractQueryFields). The + * sort metric is added to the select list so its ordering is visible in + * the result, and ordering is applied when `order_desc` is set. + */ +export default function buildQuery(formData: QueryFormData) { + const { timeseries_limit_metric, order_desc } = formData; + return buildQueryContext(formData, baseQueryObject => { + let metrics: QueryFormMetric[] = ensureIsArray(baseQueryObject.metrics); + const orderby: QueryFormOrderBy[] = []; + const sortByMetric = ensureIsArray( + timeseries_limit_metric as QueryFormMetric | QueryFormMetric[], + )[0]; + if (sortByMetric) { + const sortByLabel = getMetricLabel(sortByMetric); + if (!metrics.some(metric => getMetricLabel(metric) === sortByLabel)) { + metrics = [...metrics, sortByMetric]; + } + if (order_desc) { + orderby.push([sortByMetric, !order_desc]); + } + } + return [ + { + ...baseQueryObject, + metrics, + // own the ordering entirely: the legacy pipeline ignored residual + // orderby fields (e.g. order_by_cols left over from other viz types) + orderby: orderby.length > 0 ? orderby : undefined, + }, + ]; + }); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts index f5cb8a5c381a..c0ea250932d4 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts @@ -38,16 +38,16 @@ const metadata = new ChartMetadata({ { url: example2, urlDark: example2Dark }, ], name: t('Parallel Coordinates'), - tags: [t('Directional'), t('Legacy'), t('Relational')], + tags: [t('Directional'), t('Relational')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class ParallelCoordinatesChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactParallelCoordinates'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts new file mode 100644 index 000000000000..512bdf0f30da --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts @@ -0,0 +1,101 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const basicFormData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: 'para', + series: 'gender', + metrics: ['sum__num', 'avg__num'], +}; + +test('builds columns from series and keeps all metrics', () => { + const [query] = buildQuery(basicFormData).queries; + expect(query.columns).toEqual(['gender']); + expect(query.metrics).toEqual(['sum__num', 'avg__num']); +}); + +test('includes the secondary (color) metric in the query', () => { + const [query] = buildQuery({ + ...basicFormData, + secondary_metric: 'count', + }).queries; + expect(query.metrics).toEqual(['sum__num', 'avg__num', 'count']); +}); + +test('maps granularity_sqla from legacy saved form data', () => { + const [query] = buildQuery(basicFormData).queries; + expect(query.granularity).toEqual('ds'); +}); + +test('appends the sort metric to the select list when missing', () => { + const [query] = buildQuery({ + ...basicFormData, + timeseries_limit_metric: 'count', + order_desc: false, + }).queries; + expect(query.metrics).toEqual(['sum__num', 'avg__num', 'count']); + expect(query.orderby).toBeUndefined(); +}); + +test('does not duplicate the sort metric when already selected', () => { + const [query] = buildQuery({ + ...basicFormData, + timeseries_limit_metric: 'sum__num', + order_desc: true, + }).queries; + expect(query.metrics).toEqual(['sum__num', 'avg__num']); + expect(query.orderby).toEqual([['sum__num', false]]); +}); + +test('orders by the sort metric descending only when order_desc is set', () => { + const [query] = buildQuery({ + ...basicFormData, + timeseries_limit_metric: 'count', + order_desc: true, + }).queries; + expect(query.orderby).toEqual([['count', false]]); +}); + +test('ignores residual order_by_cols from other viz types', () => { + const [query] = buildQuery({ + ...basicFormData, + order_by_cols: ['["count", false]'], + }).queries; + expect(query.orderby).toBeUndefined(); +}); + +test('supports adhoc sort metrics', () => { + const adhocMetric = { + expressionType: 'SIMPLE' as const, + aggregate: 'SUM' as const, + column: { column_name: 'num' }, + label: 'SUM(num)', + }; + const [query] = buildQuery({ + ...basicFormData, + timeseries_limit_metric: adhocMetric, + order_desc: true, + }).queries; + expect(query.metrics).toEqual(['sum__num', 'avg__num', adhocMetric]); + expect(query.orderby).toEqual([[adhocMetric, false]]); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json new file mode 100644 index 000000000000..be84a09330ca --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false + }, + "extends": "../../../tsconfig.json", + "include": ["**/*", "../types/**/*", "../../../types/**/*"] +} From 5870259219e567318ba4acf4ff43dbc9ffc58255 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:20:15 -0700 Subject: [PATCH 05/84] feat(bullet): migrate bullet chart to v1 chart data API (#41718) Co-authored-by: Claude Code --- .../src/Bullet/buildQuery.ts | 33 +++++++ .../src/Bullet/index.ts | 4 +- .../src/transformProps.ts | 29 ++++-- .../test/Bullet.test.ts | 98 +++++++++++++++++++ 4 files changed, 152 insertions(+), 12 deletions(-) create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts new file mode 100644 index 000000000000..2db2b125834f --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts @@ -0,0 +1,33 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * Mirrors the legacy BulletViz.query_obj: a single ungrouped metric. + */ +export default function buildQuery(formData: QueryFormData) { + const { metric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: [], + metrics: metric ? [metric] : [], + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts index a66897054500..7088749c8991 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts @@ -33,16 +33,16 @@ const metadata = new ChartMetadata({ ), exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('Bullet Chart'), - tags: [t('Business'), t('Legacy'), t('Report'), t('nvd3')], + tags: [t('Business'), t('Report'), t('nvd3')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class BulletChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('../ReactNVD3'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts index edcc82a7e18f..6bf7fc46a9a2 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts @@ -17,7 +17,7 @@ * under the License. */ // @ts-nocheck -- legacy transformProps with loosely-typed formData from ChartProps -import { ChartProps, VizType } from '@superset-ui/core'; +import { ChartProps, getMetricLabel, VizType } from '@superset-ui/core'; import isTruthy from './utils/isTruthy'; import { tokenizeToNumericArray, @@ -122,15 +122,24 @@ export default function transformProps(chartProps: ChartProps) { } = formData; const rawData = queriesData[0].data || []; - const data = Array.isArray(rawData) - ? rawData.map(row => ({ - ...row, - values: Array.isArray(row.values) - ? row.values.map(value => ({ ...value })) - : row.values, - key: formatLabel(row.key, datasource.verboseMap), - })) - : rawData; + let data; + if (vizType === VizType.Bullet && Array.isArray(rawData)) { + // The legacy explore_json endpoint reshaped the single-metric result + // into `{ measures: [...] }` server-side; the v1 chart data endpoint + // returns plain records, so the reshape happens here. + const metricLabel = getMetricLabel(metric); + data = { measures: rawData.map(row => row[metricLabel]) }; + } else { + data = Array.isArray(rawData) + ? rawData.map(row => ({ + ...row, + values: Array.isArray(row.values) + ? row.values.map(value => ({ ...value })) + : row.values, + key: formatLabel(row.key, datasource.verboseMap), + })) + : rawData; + } if (vizType === VizType.Pie) { numberFormat = numberFormat || grabD3Format(datasource, metric); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts new file mode 100644 index 000000000000..18a682f078ee --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts @@ -0,0 +1,98 @@ +/** + * 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 { supersetTheme } from '@apache-superset/core/theme'; +import { ChartProps, QueryFormData, VizType } from '@superset-ui/core'; +import buildQuery from '../src/Bullet/buildQuery'; +import transformProps from '../src/transformProps'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: VizType.Bullet, + metric: 'sum__num', +}; + +describe('Bullet buildQuery', () => { + test('selects the single ungrouped metric', () => { + const [query] = buildQuery(formData).queries; + expect(query.metrics).toEqual(['sum__num']); + expect(query.columns).toEqual([]); + }); + + test('maps legacy granularity_sqla saved form data', () => { + const [query] = buildQuery(formData).queries; + expect(query.granularity).toEqual('ds'); + }); +}); + +describe('Bullet transformProps', () => { + const baseChartProps = { + width: 800, + height: 600, + datasource: { verboseMap: {} }, + theme: supersetTheme, + hooks: {}, + initialValues: {}, + }; + + test('reshapes v1 records into the measures shape', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData: { vizType: VizType.Bullet, metric: 'sum__num' }, + queriesData: [{ data: [{ sum__num: 42 }] }], + }); + const { data } = transformProps(chartProps) as { + data: { measures: number[] }; + }; + expect(data).toEqual({ measures: [42] }); + }); + + test('passes through legacy-shaped measures payloads', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData: { vizType: VizType.Bullet, metric: 'sum__num' }, + queriesData: [{ data: { measures: [42] } }], + }); + const { data } = transformProps(chartProps) as { + data: { measures: number[] }; + }; + expect(data).toEqual({ measures: [42] }); + }); + + test('reshapes using adhoc metric labels', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData: { + vizType: VizType.Bullet, + metric: { + expressionType: 'SIMPLE', + aggregate: 'SUM', + column: { column_name: 'num' }, + label: 'SUM(num)', + }, + }, + queriesData: [{ data: [{ 'SUM(num)': 7 }] }], + }); + const { data } = transformProps(chartProps) as { + data: { measures: number[] }; + }; + expect(data).toEqual({ measures: [7] }); + }); +}); From bef4680ffd1499887d74382135568f641566e8c4 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 2 Jul 2026 22:22:49 -0700 Subject: [PATCH 06/84] chore(viz): update SCOPE tracker after phase 0 + tier 1 merges Co-Authored-By: Claude Fable 5 --- SCOPE.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index 434f133b0067..b84d20c69121 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -37,13 +37,13 @@ this branch is the EOL. | # | viz_type | Plugin | Strategy | Status | |---|----------|--------|----------|--------| -| 1 | `para` | legacy-plugin-chart-parallel-coordinates | Convert in place (pass-through) | ⬜ | -| 2 | `country_map` | legacy-plugin-chart-country-map | Convert in place (select/rename) | ⬜ | -| 3 | `bullet` | legacy-preset-chart-nvd3/Bullet | Convert in place (single metric) | ⬜ | -| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | ⬜ | -| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | ⬜ | -| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | ⬜ | -| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | ⬜ | +| 1 | `para` | legacy-plugin-chart-parallel-coordinates | Convert in place (pass-through) | ✅ (merged) | +| 2 | `country_map` | legacy-plugin-chart-country-map | Convert in place (select/rename) | ✅ (merged) | +| 3 | `bullet` | legacy-preset-chart-nvd3/Bullet | Convert in place (single metric) | ✅ (merged) | +| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | 🔃 (PR open) | +| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | 🔃 (PR open) | +| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | 🔃 (PR open) | +| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | 🔃 (PR open) | | 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ⬜ | | 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ⬜ | | 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ⬜ | @@ -92,6 +92,17 @@ Per-chart checklist: (append entries as sub-PRs merge: date, sub-PR #, summary) +- 2026-07-02 — #41715 Phase 0 merged: removed orphaned `viz.py` classes + (`MapboxViz`, `MapLibreViz`, `EventFlowViz`) and nvd3 BoxPlot leftovers. +- 2026-07-02 — #41716 `para` merged: buildQuery (series→columns, secondary + metric alias, sort-metric append, explicit orderby ownership), 8 Jest tests. +- 2026-07-02 — #41717 `country_map` merged: buildQuery (entity+metric), + country_id/metric rename in transformProps with both-labels guard, 15 tests. +- 2026-07-02 — #41718 `bullet` merged: buildQuery (single ungrouped metric), + records→{measures} reshape in nvd3 transformProps Bullet branch, 5 tests. +- Open PRs: #41719 `chord`, #41720 `world_map`, #41721 `paired_ttest`, + #41723 `time_table`. + - 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. @@ -101,6 +112,7 @@ Per-chart checklist: - **compare** → migrating to `echarts_timeseries_line`; nvd3's interactive % rebasing is not preserved (flagged in UPDATES.md). - **partition** → attempting convert-in-place; if the transformProps port proves unmaintainable, fall back to proposing deprecation on dev@. - Feature branch lives on `apache/superset` so phase PRs can target it. Tracking files (SCOPE.md, UPDATES.md) are stripped before final merge. +- Tracking files are updated ONLY via direct commits to the feature branch (sub-PRs editing SCOPE.md all conflicted after the first squash-merge). ## Open questions From 39eced2c2e07fc41149f5fe4c298309011dcd2d9 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:40:18 -0700 Subject: [PATCH 07/84] feat(chord): migrate chord chart to v1 chart data API (#41719) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 43 +++++++++ .../legacy-plugin-chart-chord/src/index.ts | 4 +- .../src/transformProps.ts | 62 +++++++++++- .../test/buildQuery.test.ts | 56 +++++++++++ .../test/transformProps.test.ts | 94 +++++++++++++++++++ .../test/tsconfig.json | 8 ++ 6 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts new file mode 100644 index 000000000000..66e41a5cc018 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts @@ -0,0 +1,43 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + QueryFormColumn, + QueryFormData, +} from '@superset-ui/core'; + +/** + * Mirrors the legacy ChordViz.query_obj: one query grouped by the source + * (groupby) and target (columns) dimensions, selecting the single metric, + * ordered by that metric when sort_by_metric is on. + */ +export default function buildQuery(formData: QueryFormData) { + const { groupby, columns, metric, sort_by_metric } = formData; + const source = ensureIsArray(groupby as QueryFormColumn | QueryFormColumn[])[0]; + const target = ensureIsArray(columns as QueryFormColumn | QueryFormColumn[])[0]; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: [source, target].filter(column => column != null), + metrics: metric ? [metric] : [], + orderby: sort_by_metric && metric ? [[metric, false]] : undefined, + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts index 4f96b2f547b0..671dfd0e07f8 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts @@ -39,16 +39,16 @@ const metadata = new ChartMetadata({ }, ], name: t('Chord Diagram'), - tags: [t('Circular'), t('Legacy'), t('Proportional'), t('Relational')], + tags: [t('Circular'), t('Proportional'), t('Relational')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class ChordChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactChord'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts index 4b8fef75104e..cce03628d371 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts @@ -16,15 +16,71 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getColumnLabel, + getMetricLabel, +} from '@superset-ui/core'; + +interface ChordData { + nodes: unknown[]; + matrix: unknown[][]; +} + +/** + * Builds the square adjacency matrix that d3.chord expects, the way the + * legacy explore_json endpoint used to server-side: nodes are the union + * of source and target values, matrix[target][source] holds the + * (directional) metric value. + */ +function buildChordData( + records: Record[], + sourceLabel: string, + targetLabel: string, + metricLabel: string, +): ChordData { + const nodes = Array.from( + new Set( + records.flatMap(record => [record[sourceLabel], record[targetLabel]]), + ), + ); + const values = new Map>(); + records.forEach(record => { + const source = record[sourceLabel]; + if (!values.has(source)) { + values.set(source, new Map()); + } + values.get(source)!.set(record[targetLabel], record[metricLabel]); + }); + return { + nodes, + matrix: nodes.map(target => + nodes.map(source => values.get(source)?.get(target) ?? 0), + ), + }; +} export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; - const { yAxisFormat, colorScheme, sliceId } = formData; + const { yAxisFormat, colorScheme, sliceId, groupby, columns, metric } = + formData; + + const rawData = queriesData[0].data; + const data = + Array.isArray(rawData) && metric + ? buildChordData( + rawData, + getColumnLabel(ensureIsArray(groupby)[0]), + getColumnLabel(ensureIsArray(columns)[0]), + getMetricLabel(metric), + ) + : // legacy explore_json payloads arrive pre-shaped + rawData; return { colorScheme, - data: queriesData[0].data, + data, height, numberFormat: yAxisFormat, width, diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts new file mode 100644 index 000000000000..833eaae46a95 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts @@ -0,0 +1,56 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: 'chord', + groupby: ['source_col'], + columns: ['target_col'], + metric: 'sum__value', +}; + +test('groups by source then target and selects the single metric', () => { + const [query] = buildQuery(formData).queries; + expect(query.columns).toEqual(['source_col', 'target_col']); + expect(query.metrics).toEqual(['sum__value']); + expect(query.orderby).toBeUndefined(); +}); + +test('orders by the metric descending when sort_by_metric is on', () => { + const [query] = buildQuery({ ...formData, sort_by_metric: true }).queries; + expect(query.orderby).toEqual([['sum__value', false]]); +}); + +test('handles bare-string source/target as stored by the single-value controls', () => { + const [query] = buildQuery({ + ...formData, + groupby: 'source_col', + columns: 'target_col', + } as unknown as QueryFormData).queries; + expect(query.columns).toEqual(['source_col', 'target_col']); +}); + +test('maps legacy granularity_sqla saved form data', () => { + const [query] = buildQuery(formData).queries; + expect(query.granularity).toEqual('ds'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts new file mode 100644 index 000000000000..6e61c1a59113 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts @@ -0,0 +1,94 @@ +/** + * 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 { supersetTheme } from '@apache-superset/core/theme'; +import { ChartProps } from '@superset-ui/core'; +import transformProps from '../src/transformProps'; + +const baseChartProps = { + width: 800, + height: 600, + datasource: {}, + theme: supersetTheme, + hooks: {}, +}; + +const formData = { + groupby: 'source_col', + columns: 'target_col', + metric: 'sum__value', + yAxisFormat: '.2f', + colorScheme: 'd3Category10', + sliceId: 1, +}; + +test('builds the chord node list and matrix from v1 records', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData, + queriesData: [ + { + data: [ + { source_col: 'a', target_col: 'b', sum__value: 10 }, + { source_col: 'b', target_col: 'a', sum__value: 5 }, + { source_col: 'a', target_col: 'c', sum__value: 2 }, + ], + }, + ], + }); + const { data } = transformProps(chartProps) as { + data: { nodes: string[]; matrix: number[][] }; + }; + expect(data.nodes).toEqual(['a', 'b', 'c']); + // matrix[targetIndex][sourceIndex], matching the legacy backend layout + expect(data.matrix).toEqual([ + [0, 5, 0], // into a: from a, b, c + [10, 0, 0], // into b + [2, 0, 0], // into c + ]); +}); + +test('keeps the last value for duplicate source/target pairs', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData, + queriesData: [ + { + data: [ + { source_col: 'a', target_col: 'b', sum__value: 1 }, + { source_col: 'a', target_col: 'b', sum__value: 9 }, + ], + }, + ], + }); + const { data } = transformProps(chartProps) as { + data: { nodes: string[]; matrix: number[][] }; + }; + expect(data.matrix[1][0]).toEqual(9); +}); + +test('passes through legacy pre-shaped payloads', () => { + const legacyPayload = { nodes: ['a'], matrix: [[0]] }; + const chartProps = new ChartProps({ + ...baseChartProps, + formData, + queriesData: [{ data: legacyPayload }], + }); + const { data } = transformProps(chartProps) as { data: unknown }; + expect(data).toEqual(legacyPayload); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json new file mode 100644 index 000000000000..be84a09330ca --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false + }, + "extends": "../../../tsconfig.json", + "include": ["**/*", "../types/**/*", "../../../types/**/*"] +} From 218c64296f3a1e9d55fbf09bb779e08a6d7769ba Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:46:57 -0700 Subject: [PATCH 08/84] feat(paired-t-test): migrate paired_ttest chart to v1 chart data API (#41721) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 59 +++++++++ .../src/index.ts | 4 +- .../src/transformData.ts | 102 ++++++++++++++++ .../src/transformProps.ts | 27 ++++- .../test/buildQuery.test.ts | 56 +++++++++ .../test/transformData.test.ts | 112 ++++++++++++++++++ .../test/tsconfig.json | 8 ++ 7 files changed, 360 insertions(+), 8 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts new file mode 100644 index 000000000000..20f83fe65845 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts @@ -0,0 +1,59 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + getMetricLabel, + QueryFormData, + QueryFormMetric, + QueryFormOrderBy, +} from '@superset-ui/core'; + +/** + * Mirrors the legacy PairedTTestViz.query_obj: a timeseries query grouped + * by the group columns, selecting all metrics, with the sort metric + * appended to the select list and ordering applied when order_desc is set. + */ +export default function buildQuery(formData: QueryFormData) { + const { timeseries_limit_metric, order_desc } = formData; + return buildQueryContext(formData, baseQueryObject => { + let metrics: QueryFormMetric[] = ensureIsArray(baseQueryObject.metrics); + const orderby: QueryFormOrderBy[] = []; + const sortByMetric = ensureIsArray( + timeseries_limit_metric as QueryFormMetric | QueryFormMetric[], + )[0]; + if (sortByMetric) { + const sortByLabel = getMetricLabel(sortByMetric); + if (!metrics.some(metric => getMetricLabel(metric) === sortByLabel)) { + metrics = [...metrics, sortByMetric]; + } + if (order_desc) { + orderby.push([sortByMetric, !order_desc]); + } + } + return [ + { + ...baseQueryObject, + metrics, + is_timeseries: true, + orderby: orderby.length > 0 ? orderby : undefined, + }, + ]; + }); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts index a2e28a061147..29ad81a22ee5 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts @@ -32,16 +32,16 @@ const metadata = new ChartMetadata({ ), exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('Paired t-test Table'), - tags: [t('Legacy'), t('Statistical'), t('Tabular')], + tags: [t('Statistical'), t('Tabular')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class PairedTTestChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./PairedTTest'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts new file mode 100644 index 000000000000..d538922e8a25 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts @@ -0,0 +1,102 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +export interface TTestSeries { + group: unknown[] | string; + values: { x: unknown; y: unknown }[]; +} + +export type TTestData = Record; + +/** + * Ports the legacy PairedTTestViz.get_data reshape: pivot the timeseries + * records on the timestamp with one series per (metric, group tuple), + * padding missing timestamps with null the way pandas pivot_table did. + */ +export default function transformData( + records: Record[], + groupbyLabels: string[], + metricLabels: string[], +): TTestData { + const timestamps = Array.from( + new Set(records.map(record => record[DTTM_ALIAS])), + ).sort((a, b) => { + if (a === b) return 0; + return (a as number) < (b as number) ? -1 : 1; + }); + + const hasGroup = groupbyLabels.length > 0; + const groupKeyOf = (record: Record) => + JSON.stringify(groupbyLabels.map(label => record[label])); + + // groupKey -> tuple of group values, sorted like pandas pivot columns + const groupTuples = new Map(); + // metric -> groupKey -> timestamp -> value + const values = new Map>>(); + metricLabels.forEach(metric => values.set(metric, new Map())); + + records.forEach(record => { + const groupKey = groupKeyOf(record); + if (!groupTuples.has(groupKey)) { + groupTuples.set( + groupKey, + groupbyLabels.map(label => record[label]), + ); + } + metricLabels.forEach(metric => { + const byGroup = values.get(metric)!; + if (!byGroup.has(groupKey)) { + byGroup.set(groupKey, new Map()); + } + byGroup.get(groupKey)!.set(record[DTTM_ALIAS], record[metric]); + }); + }); + + // element-wise tuple comparison so numeric groups sort numerically, + // matching the pandas pivot column ordering + const compareTuples = (a: unknown[], b: unknown[]) => { + for (let i = 0; i < Math.min(a.length, b.length); i += 1) { + if (a[i] !== b[i]) { + if (typeof a[i] === 'number' && typeof b[i] === 'number') { + return (a[i] as number) - (b[i] as number); + } + return String(a[i]) < String(b[i]) ? -1 : 1; + } + } + return a.length - b.length; + }; + const sortedGroupKeys = Array.from(groupTuples.entries()) + .sort(([, a], [, b]) => compareTuples(a, b)) + .map(([key]) => key); + + const data: TTestData = {}; + metricLabels.forEach(metric => { + const byGroup = values.get(metric)!; + const seriesKeys = hasGroup ? sortedGroupKeys : [groupKeyOf({})]; + data[metric] = seriesKeys.map(groupKey => ({ + group: hasGroup ? groupTuples.get(groupKey)! : 'All', + values: timestamps.map(timestamp => ({ + x: timestamp, + y: byGroup.get(groupKey)?.get(timestamp) ?? null, + })), + })); + }); + return data; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts index 4decf04962cf..db95184a03cd 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getColumnLabel, + getMetricLabel, +} from '@superset-ui/core'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { formData, queriesData } = chartProps; @@ -28,15 +34,24 @@ export default function transformProps(chartProps: ChartProps) { significanceLevel, } = formData; + const metricLabels = ensureIsArray(metrics).map(getMetricLabel); + const rawData = queriesData[0].data; + // The legacy explore_json endpoint pivoted the timeseries server-side; + // v1 responses arrive as flat records and are reshaped here. + const data = Array.isArray(rawData) + ? transformData( + rawData, + ensureIsArray(groupby).map(getColumnLabel), + metricLabels, + ) + : rawData; + return { alpha: significanceLevel, - data: queriesData[0].data, + data, groups: groupby, liftValPrec: parseInt(liftvaluePrecision, 10), - metrics: (metrics as (string | { label: string })[]).map( - (metric: string | { label: string }) => - typeof metric === 'string' ? metric : metric.label, - ), + metrics: metricLabels, pValPrec: parseInt(pvaluePrecision, 10), }; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts new file mode 100644 index 000000000000..ecebf94b8b4f --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts @@ -0,0 +1,56 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_grain_sqla: 'P1D', + time_range: 'No filter', + viz_type: 'paired_ttest', + groupby: ['gender'], + metrics: ['sum__num'], +}; + +test('builds a timeseries query grouped by the group columns', () => { + const [query] = buildQuery(formData).queries; + expect(query.columns).toEqual(['gender']); + expect(query.metrics).toEqual(['sum__num']); + expect(query.is_timeseries).toBe(true); + expect(query.granularity).toEqual('ds'); +}); + +test('appends the sort metric and orders when order_desc is set', () => { + const [query] = buildQuery({ + ...formData, + timeseries_limit_metric: 'count', + order_desc: true, + }).queries; + expect(query.metrics).toEqual(['sum__num', 'count']); + expect(query.orderby).toEqual([['count', false]]); +}); + +test('ignores residual order_by_cols from other viz types', () => { + const [query] = buildQuery({ + ...formData, + order_by_cols: ['["count", false]'], + }).queries; + expect(query.orderby).toBeUndefined(); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts new file mode 100644 index 000000000000..0e2c23558c11 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts @@ -0,0 +1,112 @@ +/** + * 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 transformData from '../src/transformData'; + +const t1 = 1704067200000; +const t2 = 1704153600000; + +test('nests records per metric and group with null padding', () => { + const data = transformData( + [ + { __timestamp: t1, gender: 'boy', sum__num: 10 }, + { __timestamp: t2, gender: 'boy', sum__num: 20 }, + { __timestamp: t1, gender: 'girl', sum__num: 30 }, + // girl is missing at t2 -> padded with null + ], + ['gender'], + ['sum__num'], + ); + expect(Object.keys(data)).toEqual(['sum__num']); + expect(data.sum__num).toEqual([ + { + group: ['boy'], + values: [ + { x: t1, y: 10 }, + { x: t2, y: 20 }, + ], + }, + { + group: ['girl'], + values: [ + { x: t1, y: 30 }, + { x: t2, y: null }, + ], + }, + ]); +}); + +test('uses the All group when no groupby is set', () => { + const data = transformData( + [ + { __timestamp: t1, sum__num: 1 }, + { __timestamp: t2, sum__num: 2 }, + ], + [], + ['sum__num'], + ); + expect(data.sum__num).toEqual([ + { + group: 'All', + values: [ + { x: t1, y: 1 }, + { x: t2, y: 2 }, + ], + }, + ]); +}); + +test('handles multiple metrics and multi-column groups', () => { + const data = transformData( + [ + { __timestamp: t1, gender: 'boy', state: 'CA', sum__num: 1, count: 5 }, + { __timestamp: t1, gender: 'girl', state: 'NY', sum__num: 2, count: 6 }, + ], + ['gender', 'state'], + ['sum__num', 'count'], + ); + expect(Object.keys(data).sort()).toEqual(['count', 'sum__num']); + expect(data.count).toEqual([ + { group: ['boy', 'CA'], values: [{ x: t1, y: 5 }] }, + { group: ['girl', 'NY'], values: [{ x: t1, y: 6 }] }, + ]); +}); + +test('sorts numeric groups numerically like pandas pivot columns', () => { + const data = transformData( + [ + { __timestamp: t1, decade: 10, sum__num: 1 }, + { __timestamp: t1, decade: 2, sum__num: 2 }, + ], + ['decade'], + ['sum__num'], + ); + expect(data.sum__num.map(series => series.group)).toEqual([[2], [10]]); +}); + +test('sorts timestamps ascending like the pandas pivot index', () => { + const data = transformData( + [ + { __timestamp: t2, sum__num: 2 }, + { __timestamp: t1, sum__num: 1 }, + ], + [], + ['sum__num'], + ); + expect(data.sum__num[0].values.map(v => v.x)).toEqual([t1, t2]); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json new file mode 100644 index 000000000000..4c9211140ce6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.options.json", + "include": ["**/*"], + "compilerOptions": { + "esModuleInterop": true, + "types": ["jest", "node"] + } +} From cbbcf1bed2f61a84d46c5f83a387062206f76629 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 22:56:28 -0700 Subject: [PATCH 09/84] feat(calendar): migrate cal_heatmap chart to v1 chart data API (#41724) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 49 +++++++ .../legacy-plugin-chart-calendar/src/index.ts | 2 +- .../src/transformData.ts | 121 ++++++++++++++++++ .../src/transformProps.ts | 28 +++- .../test/buildQuery.test.ts | 45 +++++++ .../test/transformData.test.ts | 92 +++++++++++++ 6 files changed, 334 insertions(+), 3 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts new file mode 100644 index 000000000000..29fb5eaf783c --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts @@ -0,0 +1,49 @@ +/** + * 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 { buildQueryContext, QueryFormData, TimeGranularity } from '@superset-ui/core'; + +const SUBDOMAIN_TO_TIME_GRAIN: Record = { + min: TimeGranularity.MINUTE, + hour: TimeGranularity.HOUR, + day: TimeGranularity.DAY, + week: TimeGranularity.WEEK, + month: TimeGranularity.MONTH, + year: TimeGranularity.YEAR, +}; + +/** + * Mirrors the legacy CalHeatmapViz.query_obj: a timeseries query whose + * time grain is forced from the subdomain granularity control. + */ +export default function buildQuery(formData: QueryFormData) { + const { subdomain_granularity } = formData; + const timeGrain = + SUBDOMAIN_TO_TIME_GRAIN[(subdomain_granularity as string) ?? 'min'] ?? + TimeGranularity.MINUTE; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + is_timeseries: true, + extras: { + ...baseQueryObject.extras, + time_grain_sqla: timeGrain, + }, + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts index ea2ce7e47ec2..fb326c7a5d2d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts @@ -43,13 +43,13 @@ const metadata = new ChartMetadata({ ], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class CalendarChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactCalendar'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts new file mode 100644 index 000000000000..11e8c7c716ed --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts @@ -0,0 +1,121 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { DTTM_ALIAS } from '@superset-ui/core'; + +export interface CalHeatmapPayload { + data: Record>; + start: number; + domain?: string; + subdomain?: string; + range: number; +} + +const daysInMonth = (year: number, month: number) => + new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + +/** Adds calendar months, clamping the day like dateutil.relativedelta. */ +const addMonths = (date: Date, months: number): Date => { + const totalMonths = date.getUTCFullYear() * 12 + date.getUTCMonth() + months; + const year = Math.floor(totalMonths / 12); + const month = totalMonths % 12; + const day = Math.min(date.getUTCDate(), daysInMonth(year, month)); + return new Date( + Date.UTC( + year, + month, + day, + date.getUTCHours(), + date.getUTCMinutes(), + date.getUTCSeconds(), + date.getUTCMilliseconds(), + ), + ); +}; + +/** + * Calendar delta between two dates with dateutil.relativedelta semantics: + * whole months first, then remaining days (weeks = days // 7). + */ +export const calendarDelta = (start: Date, end: Date) => { + let months = + (end.getUTCFullYear() - start.getUTCFullYear()) * 12 + + (end.getUTCMonth() - start.getUTCMonth()); + if (addMonths(start, months) > end) { + months -= 1; + } + const anchor = addMonths(start, months); + const days = Math.floor( + (end.getTime() - anchor.getTime()) / (24 * 60 * 60 * 1000), + ); + return { + years: Math.trunc(months / 12), + months: months % 12, + weeks: Math.floor(days / 7), + }; +}; + +/** + * Ports the legacy CalHeatmapViz.get_data reshape: per-metric value maps + * keyed by unix seconds, plus the domain range computed from the query's + * time bounds exactly like the backend's relativedelta arithmetic. + */ +export default function transformData( + records: Record[], + metricLabels: string[], + fromDttm: number | null | undefined, + toDttm: number | null | undefined, + domain: string, + subdomain: string, +): CalHeatmapPayload { + if (fromDttm == null || toDttm == null) { + throw new Error(t('Please provide both time bounds (Since and Until)')); + } + const data: Record> = {}; + metricLabels.forEach(metric => { + const values: Record = {}; + records.forEach(record => { + const timestamp = record[DTTM_ALIAS]; + if (timestamp != null) { + values[String((timestamp as number) / 1000)] = record[metric]; + } + }); + data[metric] = values; + }); + + const start = new Date(fromDttm); + const end = new Date(toDttm); + const delta = calendarDelta(start, end); + const diffSecs = (toDttm - fromDttm) / 1000; + + let range: number; + if (domain === 'year') { + range = end.getUTCFullYear() - start.getUTCFullYear() + 1; + } else if (domain === 'month') { + range = delta.years * 12 + delta.months + 1; + } else if (domain === 'week') { + range = delta.years * 53 + delta.weeks + 1; + } else if (domain === 'day') { + range = Math.floor(diffSecs / (24 * 60 * 60)) + 1; + } else { + range = Math.floor(diffSecs / (60 * 60)) + 1; + } + + return { data, start: fromDttm, domain, subdomain, range }; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts index 74e599148ed8..30de387c7720 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts @@ -17,8 +17,15 @@ * under the License. */ -import { ChartProps, getNumberFormatter } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getMetricLabel, + getNumberFormatter, + QueryFormMetric, +} from '@superset-ui/core'; import { getFormattedUTCTime } from './utils'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { height, formData, queriesData, datasource } = chartProps; @@ -44,9 +51,26 @@ export default function transformProps(chartProps: ChartProps) { getFormattedUTCTime(ts, xAxisTimeFormat); const valueFormatter = getNumberFormatter(yAxisFormat); + // The legacy explore_json endpoint computed the per-metric value maps + // and domain range server-side; v1 responses arrive as flat records. + const { data: rawData, from_dttm: fromDttm, to_dttm: toDttm } = + queriesData[0]; + const data = Array.isArray(rawData) + ? transformData( + rawData, + ensureIsArray(formData.metrics as QueryFormMetric[]).map( + getMetricLabel, + ), + fromDttm, + toDttm, + domainGranularity, + subdomainGranularity, + ) + : rawData; + return { height, - data: queriesData[0].data, + data, cellPadding, cellRadius, cellSize, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts new file mode 100644 index 000000000000..54a95eb939c1 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts @@ -0,0 +1,45 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'Last year', + viz_type: 'cal_heatmap', + metrics: ['sum__num'], + domain_granularity: 'month', + subdomain_granularity: 'day', +}; + +test('forces the time grain from the subdomain granularity', () => { + const [query] = buildQuery(formData).queries; + expect(query.extras?.time_grain_sqla).toEqual('P1D'); + expect(query.is_timeseries).toBe(true); + expect(query.metrics).toEqual(['sum__num']); +}); + +test('defaults the time grain to minutes like the legacy backend', () => { + const [query] = buildQuery({ + ...formData, + subdomain_granularity: undefined, + }).queries; + expect(query.extras?.time_grain_sqla).toEqual('PT1M'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts new file mode 100644 index 000000000000..507d634caecc --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts @@ -0,0 +1,92 @@ +/** + * 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 transformData, { calendarDelta } from '../src/transformData'; + +const jan1 = Date.UTC(2024, 0, 1); // 1704067200000 +const jan2 = Date.UTC(2024, 0, 2); + +test('keys per-metric values by unix seconds like the legacy backend', () => { + const { data, start } = transformData( + [ + { __timestamp: jan1, sum__num: 10 }, + { __timestamp: jan2, sum__num: 20 }, + ], + ['sum__num'], + jan1, + Date.UTC(2024, 11, 31), + 'month', + 'day', + ); + expect(data.sum__num).toEqual({ + '1704067200': 10, + '1704153600': 20, + }); + expect(start).toEqual(jan1); +}); + +test('throws when time bounds are missing', () => { + expect(() => + transformData([], ['sum__num'], null, jan1, 'month', 'day'), + ).toThrow('time bounds'); +}); + +test.each([ + ['year', Date.UTC(2022, 5, 15), Date.UTC(2024, 1, 1), 3], + // Jan 15 -> Apr 10 is 2 whole months + 26 days => 2 + 1 + ['month', Date.UTC(2024, 0, 15), Date.UTC(2024, 3, 10), 3], + ['day', jan1, Date.UTC(2024, 0, 8, 12), 8], + ['hour', jan1, Date.UTC(2024, 0, 1, 5, 30), 6], +])('computes the %s domain range', (domain, start, end, expected) => { + const { range } = transformData( + [], + ['m'], + start as number, + end as number, + domain as string, + 'day', + ); + expect(range).toEqual(expected); +}); + +test('computes week ranges with relativedelta semantics', () => { + // 2024-01-01 -> 2024-03-20: 2 full months + 19 days => weeks = 2 + const { range } = transformData( + [], + ['m'], + Date.UTC(2024, 0, 1), + Date.UTC(2024, 2, 20), + 'week', + 'day', + ); + expect(range).toEqual(0 * 53 + 2 + 1); +}); + +test('calendarDelta matches dateutil for month-end clamping', () => { + // Jan 31 -> Feb 29 is a full month in dateutil terms (clamped day) + expect( + calendarDelta( + new Date(Date.UTC(2024, 0, 31)), + new Date(Date.UTC(2024, 1, 29)), + ), + ).toEqual({ + years: 0, + months: 1, + weeks: 0, + }); +}); From 93a02449dbe13c03634a9be5778eea4e843698d5 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 23:00:17 -0700 Subject: [PATCH 10/84] feat(world-map): migrate world_map chart to v1 chart data API (#41720) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 49 + .../src/countries.ts | 2030 +++++++++++++++++ .../src/index.ts | 3 +- .../src/transformData.ts | 80 + .../src/transformProps.ts | 29 +- .../test/buildQuery.test.ts | 63 + .../test/transformData.test.ts | 83 + .../test/transformProps.test.ts | 80 + 8 files changed, 2413 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts new file mode 100644 index 000000000000..4fc09db43bb8 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts @@ -0,0 +1,49 @@ +/** + * 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 { + buildQueryContext, + getMetricLabel, + QueryFormData, + QueryFormMetric, +} from '@superset-ui/core'; + +/** + * Mirrors the legacy WorldMapViz.query_obj: one query grouped by the + * country entity column selecting the main metric (and the secondary + * bubble-size metric when set), ordered by the main metric when + * sort_by_metric is on. + */ +export default function buildQuery(formData: QueryFormData) { + const { entity, metric, secondary_metric, sort_by_metric } = formData; + const metrics: QueryFormMetric[] = metric ? [metric] : []; + if ( + secondary_metric && + (!metric || getMetricLabel(secondary_metric) !== getMetricLabel(metric)) + ) { + metrics.push(secondary_metric); + } + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: entity ? [entity] : [], + metrics, + orderby: sort_by_metric && metric ? [[metric, false]] : undefined, + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts new file mode 100644 index 000000000000..2db1539bb491 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts @@ -0,0 +1,2030 @@ +/** + * 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. + */ +// Generated from superset/examples/countries.py so the world map can join +// country codes to cca3/coordinates client-side, as the legacy explore_json +// endpoint used to do server-side. + +export interface CountryInfo { + name: string; + cca2: string; + cca3: string; + cioc: string; + lat: number; + lng: number; +} + +export type CountryFieldType = 'name' | 'cca2' | 'cca3' | 'cioc'; + +export const countries: CountryInfo[] = [ + { + name: 'Angola', + cca2: 'AO', + cca3: 'AGO', + cioc: 'ANG', + lat: -12.5, + lng: 18.5, + }, + { + name: 'Algeria', + cca2: 'DZ', + cca3: 'DZA', + cioc: 'ALG', + lat: 28, + lng: 3, + }, + { + name: 'Egypt', + cca2: 'EG', + cca3: 'EGY', + cioc: 'EGY', + lat: 27, + lng: 30, + }, + { + name: 'Bangladesh', + cca2: 'BD', + cca3: 'BGD', + cioc: 'BAN', + lat: 24, + lng: 90, + }, + { + name: 'Niger', + cca2: 'NE', + cca3: 'NER', + cioc: 'NIG', + lat: 16, + lng: 8, + }, + { + name: 'Liechtenstein', + cca2: 'LI', + cca3: 'LIE', + cioc: 'LIE', + lat: 47.26666666, + lng: 9.53333333, + }, + { + name: 'Namibia', + cca2: 'NA', + cca3: 'NAM', + cioc: 'NAM', + lat: -22, + lng: 17, + }, + { + name: 'Bulgaria', + cca2: 'BG', + cca3: 'BGR', + cioc: 'BUL', + lat: 43, + lng: 25, + }, + { + name: 'Bolivia', + cca2: 'BO', + cca3: 'BOL', + cioc: 'BOL', + lat: -17, + lng: -65, + }, + { + name: 'Ghana', + cca2: 'GH', + cca3: 'GHA', + cioc: 'GHA', + lat: 8, + lng: -2, + }, + { + name: 'Cocos (Keeling) Islands', + cca2: 'CC', + cca3: 'CCK', + cioc: '', + lat: -12.5, + lng: 96.83333333, + }, + { + name: 'Pakistan', + cca2: 'PK', + cca3: 'PAK', + cioc: 'PAK', + lat: 30, + lng: 70, + }, + { + name: 'Cape Verde', + cca2: 'CV', + cca3: 'CPV', + cioc: 'CPV', + lat: 16, + lng: -24, + }, + { + name: 'Jordan', + cca2: 'JO', + cca3: 'JOR', + cioc: 'JOR', + lat: 31, + lng: 36, + }, + { + name: 'Liberia', + cca2: 'LR', + cca3: 'LBR', + cioc: 'LBR', + lat: 6.5, + lng: -9.5, + }, + { + name: 'Libya', + cca2: 'LY', + cca3: 'LBY', + cioc: 'LBA', + lat: 25, + lng: 17, + }, + { + name: 'Malaysia', + cca2: 'MY', + cca3: 'MYS', + cioc: 'MAS', + lat: 2.5, + lng: 112.5, + }, + { + name: 'Dominican Republic', + cca2: 'DO', + cca3: 'DOM', + cioc: 'DOM', + lat: 19, + lng: -70.66666666, + }, + { + name: 'Puerto Rico', + cca2: 'PR', + cca3: 'PRI', + cioc: 'PUR', + lat: 18.25, + lng: -66.5, + }, + { + name: 'Mayotte', + cca2: 'YT', + cca3: 'MYT', + cioc: '', + lat: -12.83333333, + lng: 45.16666666, + }, + { + name: 'North Korea', + cca2: 'KP', + cca3: 'PRK', + cioc: 'PRK', + lat: 40, + lng: 127, + }, + { + name: 'Palestine', + cca2: 'PS', + cca3: 'PSE', + cioc: 'PLE', + lat: 31.9, + lng: 35.2, + }, + { + name: 'Tanzania', + cca2: 'TZ', + cca3: 'TZA', + cioc: 'TAN', + lat: -6, + lng: 35, + }, + { + name: 'Botswana', + cca2: 'BW', + cca3: 'BWA', + cioc: 'BOT', + lat: -22, + lng: 24, + }, + { + name: 'Cambodia', + cca2: 'KH', + cca3: 'KHM', + cioc: 'CAM', + lat: 13, + lng: 105, + }, + { + name: 'Nicaragua', + cca2: 'NI', + cca3: 'NIC', + cioc: 'NCA', + lat: 13, + lng: -85, + }, + { + name: 'Trinidad and Tobago', + cca2: 'TT', + cca3: 'TTO', + cioc: 'TTO', + lat: 11, + lng: -61, + }, + { + name: 'Ethiopia', + cca2: 'ET', + cca3: 'ETH', + cioc: 'ETH', + lat: 8, + lng: 38, + }, + { + name: 'Paraguay', + cca2: 'PY', + cca3: 'PRY', + cioc: 'PAR', + lat: -23, + lng: -58, + }, + { + name: 'Hong Kong', + cca2: 'HK', + cca3: 'HKG', + cioc: 'HKG', + lat: 22.267, + lng: 114.188, + }, + { + name: 'Saudi Arabia', + cca2: 'SA', + cca3: 'SAU', + cioc: 'KSA', + lat: 25, + lng: 45, + }, + { + name: 'Lebanon', + cca2: 'LB', + cca3: 'LBN', + cioc: 'LIB', + lat: 33.83333333, + lng: 35.83333333, + }, + { + name: 'Slovenia', + cca2: 'SI', + cca3: 'SVN', + cioc: 'SLO', + lat: 46.11666666, + lng: 14.81666666, + }, + { + name: 'Burkina Faso', + cca2: 'BF', + cca3: 'BFA', + cioc: 'BUR', + lat: 13, + lng: -2, + }, + { + name: 'Switzerland', + cca2: 'CH', + cca3: 'CHE', + cioc: 'SUI', + lat: 47, + lng: 8, + }, + { + name: 'Mauritania', + cca2: 'MR', + cca3: 'MRT', + cioc: 'MTN', + lat: 20, + lng: -12, + }, + { + name: 'Croatia', + cca2: 'HR', + cca3: 'HRV', + cioc: 'CRO', + lat: 45.16666666, + lng: 15.5, + }, + { + name: 'Chile', + cca2: 'CL', + cca3: 'CHL', + cioc: 'CHI', + lat: -30, + lng: -71, + }, + { + name: 'China', + cca2: 'CN', + cca3: 'CHN', + cioc: 'CHN', + lat: 35, + lng: 105, + }, + { + name: 'Saint Kitts and Nevis', + cca2: 'KN', + cca3: 'KNA', + cioc: 'SKN', + lat: 17.33333333, + lng: -62.75, + }, + { + name: 'Sierra Leone', + cca2: 'SL', + cca3: 'SLE', + cioc: 'SLE', + lat: 8.5, + lng: -11.5, + }, + { + name: 'Jamaica', + cca2: 'JM', + cca3: 'JAM', + cioc: 'JAM', + lat: 18.25, + lng: -77.5, + }, + { + name: 'San Marino', + cca2: 'SM', + cca3: 'SMR', + cioc: 'SMR', + lat: 43.76666666, + lng: 12.41666666, + }, + { + name: 'Gibraltar', + cca2: 'GI', + cca3: 'GIB', + cioc: '', + lat: 36.13333333, + lng: -5.35, + }, + { + name: 'Djibouti', + cca2: 'DJ', + cca3: 'DJI', + cioc: 'DJI', + lat: 11.5, + lng: 43, + }, + { + name: 'Guinea', + cca2: 'GN', + cca3: 'GIN', + cioc: 'GUI', + lat: 11, + lng: -10, + }, + { + name: 'Finland', + cca2: 'FI', + cca3: 'FIN', + cioc: 'FIN', + lat: 64, + lng: 26, + }, + { + name: 'Uruguay', + cca2: 'UY', + cca3: 'URY', + cioc: 'URU', + lat: -33, + lng: -56, + }, + { + name: 'Thailand', + cca2: 'TH', + cca3: 'THA', + cioc: 'THA', + lat: 15, + lng: 100, + }, + { + name: 'Sao Tome and Principe', + cca2: 'ST', + cca3: 'STP', + cioc: 'STP', + lat: 1, + lng: 7, + }, + { + name: 'Seychelles', + cca2: 'SC', + cca3: 'SYC', + cioc: 'SEY', + lat: -4.58333333, + lng: 55.66666666, + }, + { + name: 'Nepal', + cca2: 'NP', + cca3: 'NPL', + cioc: 'NEP', + lat: 28, + lng: 84, + }, + { + name: 'Christmas Island', + cca2: 'CX', + cca3: 'CXR', + cioc: '', + lat: -10.5, + lng: 105.66666666, + }, + { + name: 'Laos', + cca2: 'LA', + cca3: 'LAO', + cioc: 'LAO', + lat: 18, + lng: 105, + }, + { + name: 'Yemen', + cca2: 'YE', + cca3: 'YEM', + cioc: 'YEM', + lat: 15, + lng: 48, + }, + { + name: 'Bouvet Island', + cca2: 'BV', + cca3: 'BVT', + cioc: '', + lat: -54.43333333, + lng: 3.4, + }, + { + name: 'South Africa', + cca2: 'ZA', + cca3: 'ZAF', + cioc: 'RSA', + lat: -29, + lng: 24, + }, + { + name: 'Kiribati', + cca2: 'KI', + cca3: 'KIR', + cioc: 'KIR', + lat: 1.41666666, + lng: 173, + }, + { + name: 'Philippines', + cca2: 'PH', + cca3: 'PHL', + cioc: 'PHI', + lat: 13, + lng: 122, + }, + { + name: 'Sint Maarten', + cca2: 'SX', + cca3: 'SXM', + cioc: '', + lat: 18.033333, + lng: -63.05, + }, + { + name: 'Romania', + cca2: 'RO', + cca3: 'ROU', + cioc: 'ROU', + lat: 46, + lng: 25, + }, + { + name: 'United States Virgin Islands', + cca2: 'VI', + cca3: 'VIR', + cioc: 'ISV', + lat: 18.35, + lng: -64.933333, + }, + { + name: 'Syria', + cca2: 'SY', + cca3: 'SYR', + cioc: 'SYR', + lat: 35, + lng: 38, + }, + { + name: 'Macau', + cca2: 'MO', + cca3: 'MAC', + cioc: '', + lat: 22.16666666, + lng: 113.55, + }, + { + name: 'Saint Martin', + cca2: 'MF', + cca3: 'MAF', + cioc: '', + lat: 18.08333333, + lng: -63.95, + }, + { + name: 'Malta', + cca2: 'MT', + cca3: 'MLT', + cioc: 'MLT', + lat: 35.83333333, + lng: 14.58333333, + }, + { + name: 'Kazakhstan', + cca2: 'KZ', + cca3: 'KAZ', + cioc: 'KAZ', + lat: 48, + lng: 68, + }, + { + name: 'Turks and Caicos Islands', + cca2: 'TC', + cca3: 'TCA', + cioc: '', + lat: 21.75, + lng: -71.58333333, + }, + { + name: 'French Polynesia', + cca2: 'PF', + cca3: 'PYF', + cioc: '', + lat: -15, + lng: -140, + }, + { + name: 'Niue', + cca2: 'NU', + cca3: 'NIU', + cioc: '', + lat: -19.03333333, + lng: -169.86666666, + }, + { + name: 'Dominica', + cca2: 'DM', + cca3: 'DMA', + cioc: 'DMA', + lat: 15.41666666, + lng: -61.33333333, + }, + { + name: 'Benin', + cca2: 'BJ', + cca3: 'BEN', + cioc: 'BEN', + lat: 9.5, + lng: 2.25, + }, + { + name: 'French Guiana', + cca2: 'GF', + cca3: 'GUF', + cioc: '', + lat: 4, + lng: -53, + }, + { + name: 'Belgium', + cca2: 'BE', + cca3: 'BEL', + cioc: 'BEL', + lat: 50.83333333, + lng: 4, + }, + { + name: 'Montserrat', + cca2: 'MS', + cca3: 'MSR', + cioc: '', + lat: 16.75, + lng: -62.2, + }, + { + name: 'Togo', + cca2: 'TG', + cca3: 'TGO', + cioc: 'TOG', + lat: 8, + lng: 1.16666666, + }, + { + name: 'Germany', + cca2: 'DE', + cca3: 'DEU', + cioc: 'GER', + lat: 51, + lng: 9, + }, + { + name: 'Guam', + cca2: 'GU', + cca3: 'GUM', + cioc: 'GUM', + lat: 13.46666666, + lng: 144.78333333, + }, + { + name: 'Sri Lanka', + cca2: 'LK', + cca3: 'LKA', + cioc: 'SRI', + lat: 7, + lng: 81, + }, + { + name: 'South Sudan', + cca2: 'SS', + cca3: 'SSD', + cioc: '', + lat: 7, + lng: 30, + }, + { + name: 'Falkland Islands', + cca2: 'FK', + cca3: 'FLK', + cioc: '', + lat: -51.75, + lng: -59, + }, + { + name: 'United Kingdom', + cca2: 'GB', + cca3: 'GBR', + cioc: 'GBR', + lat: 54, + lng: -2, + }, + { + name: 'Guyana', + cca2: 'GY', + cca3: 'GUY', + cioc: 'GUY', + lat: 5, + lng: -59, + }, + { + name: 'Costa Rica', + cca2: 'CR', + cca3: 'CRI', + cioc: 'CRC', + lat: 10, + lng: -84, + }, + { + name: 'Cameroon', + cca2: 'CM', + cca3: 'CMR', + cioc: 'CMR', + lat: 6, + lng: 12, + }, + { + name: 'Morocco', + cca2: 'MA', + cca3: 'MAR', + cioc: 'MAR', + lat: 32, + lng: -5, + }, + { + name: 'Northern Mariana Islands', + cca2: 'MP', + cca3: 'MNP', + cioc: '', + lat: 15.2, + lng: 145.75, + }, + { + name: 'Lesotho', + cca2: 'LS', + cca3: 'LSO', + cioc: 'LES', + lat: -29.5, + lng: 28.5, + }, + { + name: 'Hungary', + cca2: 'HU', + cca3: 'HUN', + cioc: 'HUN', + lat: 47, + lng: 20, + }, + { + name: 'Turkmenistan', + cca2: 'TM', + cca3: 'TKM', + cioc: 'TKM', + lat: 40, + lng: 60, + }, + { + name: 'Suriname', + cca2: 'SR', + cca3: 'SUR', + cioc: 'SUR', + lat: 4, + lng: -56, + }, + { + name: 'Netherlands', + cca2: 'NL', + cca3: 'NLD', + cioc: 'NED', + lat: 52.5, + lng: 5.75, + }, + { + name: 'Bermuda', + cca2: 'BM', + cca3: 'BMU', + cioc: 'BER', + lat: 32.33333333, + lng: -64.75, + }, + { + name: 'Heard Island and McDonald Islands', + cca2: 'HM', + cca3: 'HMD', + cioc: '', + lat: -53.1, + lng: 72.51666666, + }, + { + name: 'Chad', + cca2: 'TD', + cca3: 'TCD', + cioc: 'CHA', + lat: 15, + lng: 19, + }, + { + name: 'Georgia', + cca2: 'GE', + cca3: 'GEO', + cioc: 'GEO', + lat: 42, + lng: 43.5, + }, + { + name: 'Montenegro', + cca2: 'ME', + cca3: 'MNE', + cioc: 'MNE', + lat: 42.5, + lng: 19.3, + }, + { + name: 'Mongolia', + cca2: 'MN', + cca3: 'MNG', + cioc: 'MGL', + lat: 46, + lng: 105, + }, + { + name: 'Marshall Islands', + cca2: 'MH', + cca3: 'MHL', + cioc: 'MHL', + lat: 9, + lng: 168, + }, + { + name: 'Martinique', + cca2: 'MQ', + cca3: 'MTQ', + cioc: '', + lat: 14.666667, + lng: -61, + }, + { + name: 'Belize', + cca2: 'BZ', + cca3: 'BLZ', + cioc: 'BIZ', + lat: 17.25, + lng: -88.75, + }, + { + name: 'Norfolk Island', + cca2: 'NF', + cca3: 'NFK', + cioc: '', + lat: -29.03333333, + lng: 167.95, + }, + { + name: 'Myanmar', + cca2: 'MM', + cca3: 'MMR', + cioc: 'MYA', + lat: 22, + lng: 98, + }, + { + name: 'Afghanistan', + cca2: 'AF', + cca3: 'AFG', + cioc: 'AFG', + lat: 33, + lng: 65, + }, + { + name: 'Burundi', + cca2: 'BI', + cca3: 'BDI', + cioc: 'BDI', + lat: -3.5, + lng: 30, + }, + { + name: 'British Virgin Islands', + cca2: 'VG', + cca3: 'VGB', + cioc: 'IVB', + lat: 18.431383, + lng: -64.62305, + }, + { + name: 'Belarus', + cca2: 'BY', + cca3: 'BLR', + cioc: 'BLR', + lat: 53, + lng: 28, + }, + { + name: 'Saint Barthelemy', + cca2: 'BL', + cca3: 'BLM', + cioc: '', + lat: 18.5, + lng: -63.41666666, + }, + { + name: 'Grenada', + cca2: 'GD', + cca3: 'GRD', + cioc: 'GRN', + lat: 12.11666666, + lng: -61.66666666, + }, + { + name: 'Tokelau', + cca2: 'TK', + cca3: 'TKL', + cioc: '', + lat: -9, + lng: -172, + }, + { + name: 'Greece', + cca2: 'GR', + cca3: 'GRC', + cioc: 'GRE', + lat: 39, + lng: 22, + }, + { + name: 'Russia', + cca2: 'RU', + cca3: 'RUS', + cioc: 'RUS', + lat: 60, + lng: 100, + }, + { + name: 'Greenland', + cca2: 'GL', + cca3: 'GRL', + cioc: '', + lat: 72, + lng: -40, + }, + { + name: 'Andorra', + cca2: 'AD', + cca3: 'AND', + cioc: 'AND', + lat: 42.5, + lng: 1.5, + }, + { + name: 'Mozambique', + cca2: 'MZ', + cca3: 'MOZ', + cioc: 'MOZ', + lat: -18.25, + lng: 35, + }, + { + name: 'Tajikistan', + cca2: 'TJ', + cca3: 'TJK', + cioc: 'TJK', + lat: 39, + lng: 71, + }, + { + name: 'Haiti', + cca2: 'HT', + cca3: 'HTI', + cioc: 'HAI', + lat: 19, + lng: -72.41666666, + }, + { + name: 'Mexico', + cca2: 'MX', + cca3: 'MEX', + cioc: 'MEX', + lat: 23, + lng: -102, + }, + { + name: 'Zimbabwe', + cca2: 'ZW', + cca3: 'ZWE', + cioc: 'ZIM', + lat: -20, + lng: 30, + }, + { + name: 'Saint Lucia', + cca2: 'LC', + cca3: 'LCA', + cioc: 'LCA', + lat: 13.88333333, + lng: -60.96666666, + }, + { + name: 'India', + cca2: 'IN', + cca3: 'IND', + cioc: 'IND', + lat: 20, + lng: 77, + }, + { + name: 'Latvia', + cca2: 'LV', + cca3: 'LVA', + cioc: 'LAT', + lat: 57, + lng: 25, + }, + { + name: 'Bhutan', + cca2: 'BT', + cca3: 'BTN', + cioc: 'BHU', + lat: 27.5, + lng: 90.5, + }, + { + name: 'Saint Vincent and the Grenadines', + cca2: 'VC', + cca3: 'VCT', + cioc: 'VIN', + lat: 13.25, + lng: -61.2, + }, + { + name: 'Vietnam', + cca2: 'VN', + cca3: 'VNM', + cioc: 'VIE', + lat: 16.16666666, + lng: 107.83333333, + }, + { + name: 'Norway', + cca2: 'NO', + cca3: 'NOR', + cioc: 'NOR', + lat: 62, + lng: 10, + }, + { + name: 'Czech Republic', + cca2: 'CZ', + cca3: 'CZE', + cioc: 'CZE', + lat: 49.75, + lng: 15.5, + }, + { + name: 'French Southern and Antarctic Lands', + cca2: 'TF', + cca3: 'ATF', + cioc: '', + lat: -49.25, + lng: 69.167, + }, + { + name: 'Antigua and Barbuda', + cca2: 'AG', + cca3: 'ATG', + cioc: 'ANT', + lat: 17.05, + lng: -61.8, + }, + { + name: 'Fiji', + cca2: 'FJ', + cca3: 'FJI', + cioc: 'FIJ', + lat: -18, + lng: 175, + }, + { + name: 'British Indian Ocean Territory', + cca2: 'IO', + cca3: 'IOT', + cioc: '', + lat: -6, + lng: 71.5, + }, + { + name: 'Honduras', + cca2: 'HN', + cca3: 'HND', + cioc: 'HON', + lat: 15, + lng: -86.5, + }, + { + name: 'Mauritius', + cca2: 'MU', + cca3: 'MUS', + cioc: 'MRI', + lat: -20.28333333, + lng: 57.55, + }, + { + name: 'Antarctica', + cca2: 'AQ', + cca3: 'ATA', + cioc: '', + lat: -90, + lng: 0, + }, + { + name: 'Luxembourg', + cca2: 'LU', + cca3: 'LUX', + cioc: 'LUX', + lat: 49.75, + lng: 6.16666666, + }, + { + name: 'Israel', + cca2: 'IL', + cca3: 'ISR', + cioc: 'ISR', + lat: 31.47, + lng: 35.13, + }, + { + name: 'Micronesia', + cca2: 'FM', + cca3: 'FSM', + cioc: 'FSM', + lat: 6.91666666, + lng: 158.25, + }, + { + name: 'Peru', + cca2: 'PE', + cca3: 'PER', + cioc: 'PER', + lat: -10, + lng: -76, + }, + { + name: 'Reunion', + cca2: 'RE', + cca3: 'REU', + cioc: '', + lat: -21.15, + lng: 55.5, + }, + { + name: 'Indonesia', + cca2: 'ID', + cca3: 'IDN', + cioc: 'INA', + lat: -5, + lng: 120, + }, + { + name: 'Vanuatu', + cca2: 'VU', + cca3: 'VUT', + cioc: 'VAN', + lat: -16, + lng: 167, + }, + { + name: 'Macedonia', + cca2: 'MK', + cca3: 'MKD', + cioc: 'MKD', + lat: 41.83333333, + lng: 22, + }, + { + name: 'DR Congo', + cca2: 'CD', + cca3: 'COD', + cioc: 'COD', + lat: 0, + lng: 25, + }, + { + name: 'Republic of the Congo', + cca2: 'CG', + cca3: 'COG', + cioc: 'CGO', + lat: -1, + lng: 15, + }, + { + name: 'Iceland', + cca2: 'IS', + cca3: 'ISL', + cioc: 'ISL', + lat: 65, + lng: -18, + }, + { + name: 'Guadeloupe', + cca2: 'GP', + cca3: 'GLP', + cioc: '', + lat: 16.25, + lng: -61.583333, + }, + { + name: 'Cook Islands', + cca2: 'CK', + cca3: 'COK', + cioc: 'COK', + lat: -21.23333333, + lng: -159.76666666, + }, + { + name: 'Comoros', + cca2: 'KM', + cca3: 'COM', + cioc: 'COM', + lat: -12.16666666, + lng: 44.25, + }, + { + name: 'Colombia', + cca2: 'CO', + cca3: 'COL', + cioc: 'COL', + lat: 4, + lng: -72, + }, + { + name: 'Nigeria', + cca2: 'NG', + cca3: 'NGA', + cioc: 'NGR', + lat: 10, + lng: 8, + }, + { + name: 'Timor-Leste', + cca2: 'TL', + cca3: 'TLS', + cioc: 'TLS', + lat: -8.83333333, + lng: 125.91666666, + }, + { + name: 'Taiwan', + cca2: 'TW', + cca3: 'TWN', + cioc: 'TPE', + lat: 23.5, + lng: 121, + }, + { + name: 'Portugal', + cca2: 'PT', + cca3: 'PRT', + cioc: 'POR', + lat: 39.5, + lng: -8, + }, + { + name: 'Moldova', + cca2: 'MD', + cca3: 'MDA', + cioc: 'MDA', + lat: 47, + lng: 29, + }, + { + name: 'Guernsey', + cca2: 'GG', + cca3: 'GGY', + cioc: '', + lat: 49.46666666, + lng: -2.58333333, + }, + { + name: 'Madagascar', + cca2: 'MG', + cca3: 'MDG', + cioc: 'MAD', + lat: -20, + lng: 47, + }, + { + name: 'Ecuador', + cca2: 'EC', + cca3: 'ECU', + cioc: 'ECU', + lat: -2, + lng: -77.5, + }, + { + name: 'Senegal', + cca2: 'SN', + cca3: 'SEN', + cioc: 'SEN', + lat: 14, + lng: -14, + }, + { + name: 'New Zealand', + cca2: 'NZ', + cca3: 'NZL', + cioc: 'NZL', + lat: -41, + lng: 174, + }, + { + name: 'Maldives', + cca2: 'MV', + cca3: 'MDV', + cioc: 'MDV', + lat: 3.25, + lng: 73, + }, + { + name: 'American Samoa', + cca2: 'AS', + cca3: 'ASM', + cioc: 'ASA', + lat: -14.33333333, + lng: -170, + }, + { + name: 'Saint Pierre and Miquelon', + cca2: 'PM', + cca3: 'SPM', + cioc: '', + lat: 46.83333333, + lng: -56.33333333, + }, + { + name: 'Curacao', + cca2: 'CW', + cca3: 'CUW', + cioc: '', + lat: 12.116667, + lng: -68.933333, + }, + { + name: 'France', + cca2: 'FR', + cca3: 'FRA', + cioc: 'FRA', + lat: 46, + lng: 2, + }, + { + name: 'Lithuania', + cca2: 'LT', + cca3: 'LTU', + cioc: 'LTU', + lat: 56, + lng: 24, + }, + { + name: 'Rwanda', + cca2: 'RW', + cca3: 'RWA', + cioc: 'RWA', + lat: -2, + lng: 30, + }, + { + name: 'Zambia', + cca2: 'ZM', + cca3: 'ZMB', + cioc: 'ZAM', + lat: -15, + lng: 30, + }, + { + name: 'Gambia', + cca2: 'GM', + cca3: 'GMB', + cioc: 'GAM', + lat: 13.46666666, + lng: -16.56666666, + }, + { + name: 'Wallis and Futuna', + cca2: 'WF', + cca3: 'WLF', + cioc: '', + lat: -13.3, + lng: -176.2, + }, + { + name: 'Jersey', + cca2: 'JE', + cca3: 'JEY', + cioc: '', + lat: 49.25, + lng: -2.16666666, + }, + { + name: 'Faroe Islands', + cca2: 'FO', + cca3: 'FRO', + cioc: '', + lat: 62, + lng: -7, + }, + { + name: 'Guatemala', + cca2: 'GT', + cca3: 'GTM', + cioc: 'GUA', + lat: 15.5, + lng: -90.25, + }, + { + name: 'Denmark', + cca2: 'DK', + cca3: 'DNK', + cioc: 'DEN', + lat: 56, + lng: 10, + }, + { + name: 'Isle of Man', + cca2: 'IM', + cca3: 'IMN', + cioc: '', + lat: 54.25, + lng: -4.5, + }, + { + name: 'Australia', + cca2: 'AU', + cca3: 'AUS', + cioc: 'AUS', + lat: -27, + lng: 133, + }, + { + name: 'Austria', + cca2: 'AT', + cca3: 'AUT', + cioc: 'AUT', + lat: 47.33333333, + lng: 13.33333333, + }, + { + name: 'Svalbard and Jan Mayen', + cca2: 'SJ', + cca3: 'SJM', + cioc: '', + lat: 78, + lng: 20, + }, + { + name: 'Venezuela', + cca2: 'VE', + cca3: 'VEN', + cioc: 'VEN', + lat: 8, + lng: -66, + }, + { + name: 'Kosovo', + cca2: 'XK', + cca3: 'UNK', + cioc: 'KOS', + lat: 42.666667, + lng: 21.166667, + }, + { + name: 'Palau', + cca2: 'PW', + cca3: 'PLW', + cioc: 'PLW', + lat: 7.5, + lng: 134.5, + }, + { + name: 'Kenya', + cca2: 'KE', + cca3: 'KEN', + cioc: 'KEN', + lat: 1, + lng: 38, + }, + { + name: 'Samoa', + cca2: 'WS', + cca3: 'WSM', + cioc: 'SAM', + lat: -13.58333333, + lng: -172.33333333, + }, + { + name: 'Turkey', + cca2: 'TR', + cca3: 'TUR', + cioc: 'TUR', + lat: 39, + lng: 35, + }, + { + name: 'Albania', + cca2: 'AL', + cca3: 'ALB', + cioc: 'ALB', + lat: 41, + lng: 20, + }, + { + name: 'Oman', + cca2: 'OM', + cca3: 'OMN', + cioc: 'OMA', + lat: 21, + lng: 57, + }, + { + name: 'Tuvalu', + cca2: 'TV', + cca3: 'TUV', + cioc: 'TUV', + lat: -8, + lng: 178, + }, + { + name: 'Aland Islands', + cca2: 'AX', + cca3: 'ALA', + cioc: '', + lat: 60.116667, + lng: 19.9, + }, + { + name: 'Brunei', + cca2: 'BN', + cca3: 'BRN', + cioc: 'BRU', + lat: 4.5, + lng: 114.66666666, + }, + { + name: 'Tunisia', + cca2: 'TN', + cca3: 'TUN', + cioc: 'TUN', + lat: 34, + lng: 9, + }, + { + name: 'Pitcairn Islands', + cca2: 'PN', + cca3: 'PCN', + cioc: '', + lat: -25.06666666, + lng: -130.1, + }, + { + name: 'Barbados', + cca2: 'BB', + cca3: 'BRB', + cioc: 'BAR', + lat: 13.16666666, + lng: -59.53333333, + }, + { + name: 'Brazil', + cca2: 'BR', + cca3: 'BRA', + cioc: 'BRA', + lat: -10, + lng: -55, + }, + { + name: 'Ivory Coast', + cca2: 'CI', + cca3: 'CIV', + cioc: 'CIV', + lat: 8, + lng: -5, + }, + { + name: 'Serbia', + cca2: 'RS', + cca3: 'SRB', + cioc: 'SRB', + lat: 44, + lng: 21, + }, + { + name: 'Equatorial Guinea', + cca2: 'GQ', + cca3: 'GNQ', + cioc: 'GEQ', + lat: 2, + lng: 10, + }, + { + name: 'United States', + cca2: 'US', + cca3: 'USA', + cioc: 'USA', + lat: 38, + lng: -97, + }, + { + name: 'Qatar', + cca2: 'QA', + cca3: 'QAT', + cioc: 'QAT', + lat: 25.5, + lng: 51.25, + }, + { + name: 'Sweden', + cca2: 'SE', + cca3: 'SWE', + cioc: 'SWE', + lat: 62, + lng: 15, + }, + { + name: 'Azerbaijan', + cca2: 'AZ', + cca3: 'AZE', + cioc: 'AZE', + lat: 40.5, + lng: 47.5, + }, + { + name: 'Guinea-Bissau', + cca2: 'GW', + cca3: 'GNB', + cioc: 'GBS', + lat: 12, + lng: -15, + }, + { + name: 'Swaziland', + cca2: 'SZ', + cca3: 'SWZ', + cioc: 'SWZ', + lat: -26.5, + lng: 31.5, + }, + { + name: 'Tonga', + cca2: 'TO', + cca3: 'TON', + cioc: 'TGA', + lat: -20, + lng: -175, + }, + { + name: 'Canada', + cca2: 'CA', + cca3: 'CAN', + cioc: 'CAN', + lat: 60, + lng: -95, + }, + { + name: 'Ukraine', + cca2: 'UA', + cca3: 'UKR', + cioc: 'UKR', + lat: 49, + lng: 32, + }, + { + name: 'South Korea', + cca2: 'KR', + cca3: 'KOR', + cioc: 'KOR', + lat: 37, + lng: 127.5, + }, + { + name: 'Anguilla', + cca2: 'AI', + cca3: 'AIA', + cioc: '', + lat: 18.25, + lng: -63.16666666, + }, + { + name: 'Central African Republic', + cca2: 'CF', + cca3: 'CAF', + cioc: 'CAF', + lat: 7, + lng: 21, + }, + { + name: 'Slovakia', + cca2: 'SK', + cca3: 'SVK', + cioc: 'SVK', + lat: 48.66666666, + lng: 19.5, + }, + { + name: 'Cyprus', + cca2: 'CY', + cca3: 'CYP', + cioc: 'CYP', + lat: 35, + lng: 33, + }, + { + name: 'Bosnia and Herzegovina', + cca2: 'BA', + cca3: 'BIH', + cioc: 'BIH', + lat: 44, + lng: 18, + }, + { + name: 'Singapore', + cca2: 'SG', + cca3: 'SGP', + cioc: 'SIN', + lat: 1.36666666, + lng: 103.8, + }, + { + name: 'South Georgia', + cca2: 'GS', + cca3: 'SGS', + cioc: '', + lat: -54.5, + lng: -37, + }, + { + name: 'Somalia', + cca2: 'SO', + cca3: 'SOM', + cioc: 'SOM', + lat: 10, + lng: 49, + }, + { + name: 'Uzbekistan', + cca2: 'UZ', + cca3: 'UZB', + cioc: 'UZB', + lat: 41, + lng: 64, + }, + { + name: 'Eritrea', + cca2: 'ER', + cca3: 'ERI', + cioc: 'ERI', + lat: 15, + lng: 39, + }, + { + name: 'Poland', + cca2: 'PL', + cca3: 'POL', + cioc: 'POL', + lat: 52, + lng: 20, + }, + { + name: 'Kuwait', + cca2: 'KW', + cca3: 'KWT', + cioc: 'KUW', + lat: 29.5, + lng: 45.75, + }, + { + name: 'Gabon', + cca2: 'GA', + cca3: 'GAB', + cioc: 'GAB', + lat: -1, + lng: 11.75, + }, + { + name: 'Cayman Islands', + cca2: 'KY', + cca3: 'CYM', + cioc: 'CAY', + lat: 19.5, + lng: -80.5, + }, + { + name: 'Vatican City', + cca2: 'VA', + cca3: 'VAT', + cioc: '', + lat: 41.9, + lng: 12.45, + }, + { + name: 'Estonia', + cca2: 'EE', + cca3: 'EST', + cioc: 'EST', + lat: 59, + lng: 26, + }, + { + name: 'Malawi', + cca2: 'MW', + cca3: 'MWI', + cioc: 'MAW', + lat: -13.5, + lng: 34, + }, + { + name: 'Spain', + cca2: 'ES', + cca3: 'ESP', + cioc: 'ESP', + lat: 40, + lng: -4, + }, + { + name: 'Iraq', + cca2: 'IQ', + cca3: 'IRQ', + cioc: 'IRQ', + lat: 33, + lng: 44, + }, + { + name: 'El Salvador', + cca2: 'SV', + cca3: 'SLV', + cioc: 'ESA', + lat: 13.83333333, + lng: -88.91666666, + }, + { + name: 'Mali', + cca2: 'ML', + cca3: 'MLI', + cioc: 'MLI', + lat: 17, + lng: -4, + }, + { + name: 'Ireland', + cca2: 'IE', + cca3: 'IRL', + cioc: 'IRL', + lat: 53, + lng: -8, + }, + { + name: 'Iran', + cca2: 'IR', + cca3: 'IRN', + cioc: 'IRI', + lat: 32, + lng: 53, + }, + { + name: 'Aruba', + cca2: 'AW', + cca3: 'ABW', + cioc: 'ARU', + lat: 12.5, + lng: -69.96666666, + }, + { + name: 'Papua New Guinea', + cca2: 'PG', + cca3: 'PNG', + cioc: 'PNG', + lat: -6, + lng: 147, + }, + { + name: 'Panama', + cca2: 'PA', + cca3: 'PAN', + cioc: 'PAN', + lat: 9, + lng: -80, + }, + { + name: 'Sudan', + cca2: 'SD', + cca3: 'SDN', + cioc: 'SUD', + lat: 15, + lng: 30, + }, + { + name: 'Solomon Islands', + cca2: 'SB', + cca3: 'SLB', + cioc: 'SOL', + lat: -8, + lng: 159, + }, + { + name: 'Western Sahara', + cca2: 'EH', + cca3: 'ESH', + cioc: '', + lat: 24.5, + lng: -13, + }, + { + name: 'Monaco', + cca2: 'MC', + cca3: 'MCO', + cioc: 'MON', + lat: 43.73333333, + lng: 7.4, + }, + { + name: 'Italy', + cca2: 'IT', + cca3: 'ITA', + cioc: 'ITA', + lat: 42.83333333, + lng: 12.83333333, + }, + { + name: 'Japan', + cca2: 'JP', + cca3: 'JPN', + cioc: 'JPN', + lat: 36, + lng: 138, + }, + { + name: 'Kyrgyzstan', + cca2: 'KG', + cca3: 'KGZ', + cioc: 'KGZ', + lat: 41, + lng: 75, + }, + { + name: 'Uganda', + cca2: 'UG', + cca3: 'UGA', + cioc: 'UGA', + lat: 1, + lng: 32, + }, + { + name: 'New Caledonia', + cca2: 'NC', + cca3: 'NCL', + cioc: '', + lat: -21.5, + lng: 165.5, + }, + { + name: 'United Arab Emirates', + cca2: 'AE', + cca3: 'ARE', + cioc: 'UAE', + lat: 24, + lng: 54, + }, + { + name: 'Argentina', + cca2: 'AR', + cca3: 'ARG', + cioc: 'ARG', + lat: -34, + lng: -64, + }, + { + name: 'Bahamas', + cca2: 'BS', + cca3: 'BHS', + cioc: 'BAH', + lat: 24.25, + lng: -76, + }, + { + name: 'Bahrain', + cca2: 'BH', + cca3: 'BHR', + cioc: 'BRN', + lat: 26, + lng: 50.55, + }, + { + name: 'Armenia', + cca2: 'AM', + cca3: 'ARM', + cioc: 'ARM', + lat: 40, + lng: 45, + }, + { + name: 'Nauru', + cca2: 'NR', + cca3: 'NRU', + cioc: 'NRU', + lat: -0.53333333, + lng: 166.91666666, + }, + { + name: 'Cuba', + cca2: 'CU', + cca3: 'CUB', + cioc: 'CUB', + lat: 21.5, + lng: -80, + }, +]; + +const lookups: Record> = { + name: new Map(), + cca2: new Map(), + cca3: new Map(), + cioc: new Map(), +}; +(Object.keys(lookups) as CountryFieldType[]).forEach(field => { + countries.forEach(country => { + lookups[field].set(country[field].toLowerCase(), country); + }); +}); + +export function getCountry( + field: string, + symbol: string, +): CountryInfo | undefined { + return lookups[field as CountryFieldType]?.get(symbol.toLowerCase()); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts index 4b53c5cb9ec2..f10369087953 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts @@ -42,7 +42,6 @@ const metadata = new ChartMetadata({ t('2D'), t('Comparison'), t('Intensity'), - t('Legacy'), t('Multi-Dimensions'), t('Multi-Layers'), t('Multi-Variables'), @@ -51,7 +50,6 @@ const metadata = new ChartMetadata({ ], thumbnail, thumbnailDark, - useLegacyApi: true, behaviors: [ Behavior.InteractiveChart, Behavior.DrillToDetail, @@ -63,6 +61,7 @@ export default class WorldMapChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactWorldMap'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts new file mode 100644 index 000000000000..f9d584f9f96e --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts @@ -0,0 +1,80 @@ +/** + * 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 { getColumnLabel, getMetricLabel } from '@superset-ui/core'; +import { getCountry } from './countries'; + +export interface WorldMapDataRow { + country: string; + m1: unknown; + m2?: unknown; + code?: string; + latitude?: number; + longitude?: number; + name?: string; +} + +/** + * Ports the legacy WorldMapViz.get_data reshape: rename the entity and + * metric columns to country/m1/m2 and join country metadata (cca3 code, + * coordinates, display name) from the bundled country list. Unmatched + * countries become "XXX", exactly as the backend did. + */ +export default function transformData( + records: Record[], + options: { + entity?: string; + metric?: unknown; + secondaryMetric?: unknown; + countryFieldtype?: string; + }, +): WorldMapDataRow[] { + const entityLabel = getColumnLabel(options.entity ?? ''); + const metricLabel = getMetricLabel(options.metric as never); + const secondaryLabel = options.secondaryMetric + ? getMetricLabel(options.secondaryMetric as never) + : undefined; + const fieldtype = options.countryFieldtype; + + return records.map(record => { + const row: WorldMapDataRow = { + country: record[entityLabel] as string, + m1: record[metricLabel], + }; + if (secondaryLabel) { + row.m2 = + secondaryLabel === metricLabel + ? record[metricLabel] + : record[secondaryLabel]; + } + const countryInfo = + typeof row.country === 'string' && fieldtype + ? getCountry(fieldtype, row.country) + : undefined; + if (countryInfo) { + row.code = countryInfo[fieldtype as keyof typeof countryInfo] as string; + row.country = countryInfo.cca3; + row.latitude = countryInfo.lat; + row.longitude = countryInfo.lng; + row.name = countryInfo.name; + } else { + row.country = 'XXX'; + } + return row; + }); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts index 16d84a444008..65b255a20987 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts @@ -17,7 +17,13 @@ * under the License. */ import tinycolor from 'tinycolor2'; -import { ChartProps, getValueFormatter } from '@superset-ui/core'; +import { + ChartProps, + getColumnLabel, + getMetricLabel, + getValueFormatter, +} from '@superset-ui/core'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { @@ -43,6 +49,7 @@ export default function transformProps(chartProps: ChartProps) { colorScheme, sliceId, metric, + secondaryMetric, yAxisFormat, currencyFormat, } = formData; @@ -52,7 +59,25 @@ export default function transformProps(chartProps: ChartProps) { columnFormats = {}, currencyCodeColumn, } = datasource; - const { data, detected_currency: detectedCurrency } = queriesData[0]; + const { data: rawData, detected_currency: detectedCurrency } = queriesData[0]; + + // The legacy explore_json endpoint joined country metadata server-side; + // rows carrying both the entity and metric labels are v1 records that + // still need that join, anything else is a pre-shaped legacy payload. + const entityLabel = getColumnLabel(entity); + const metricLabel = getMetricLabel(metric); + const data = + Array.isArray(rawData) && + rawData.length > 0 && + entityLabel in rawData[0] && + metricLabel in rawData[0] + ? transformData(rawData, { + entity, + metric, + secondaryMetric, + countryFieldtype, + }) + : rawData; const formatter = getValueFormatter( metric, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts new file mode 100644 index 000000000000..d0b1bdfa76bd --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts @@ -0,0 +1,63 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: 'world_map', + entity: 'country_code', + country_fieldtype: 'cca3', + metric: 'sum__num', +}; + +test('groups by the entity column and selects the metric', () => { + const [query] = buildQuery(formData).queries; + expect(query.columns).toEqual(['country_code']); + expect(query.metrics).toEqual(['sum__num']); + expect(query.orderby).toBeUndefined(); +}); + +test('includes the secondary bubble metric when distinct', () => { + const [query] = buildQuery({ + ...formData, + secondary_metric: 'count', + }).queries; + expect(query.metrics).toEqual(['sum__num', 'count']); +}); + +test('does not duplicate the metric when secondary equals primary', () => { + const [query] = buildQuery({ + ...formData, + secondary_metric: 'sum__num', + }).queries; + expect(query.metrics).toEqual(['sum__num']); +}); + +test('orders by the metric descending when sort_by_metric is on', () => { + const [query] = buildQuery({ ...formData, sort_by_metric: true }).queries; + expect(query.orderby).toEqual([['sum__num', false]]); +}); + +test('maps legacy granularity_sqla saved form data', () => { + const [query] = buildQuery(formData).queries; + expect(query.granularity).toEqual('ds'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts new file mode 100644 index 000000000000..099c32c6b80b --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts @@ -0,0 +1,83 @@ +/** + * 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 transformData from '../src/transformData'; + +const formData = { + entity: 'country_code', + metric: 'sum__num', + countryFieldtype: 'cca3', +}; + +test('joins country metadata the way the legacy backend did', () => { + const rows = transformData([{ country_code: 'FRA', sum__num: 42 }], formData); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + country: 'FRA', + code: 'FRA', + name: 'France', + m1: 42, + }); + expect(typeof rows[0].latitude).toBe('number'); + expect(typeof rows[0].longitude).toBe('number'); + expect(rows[0].m2).toBeUndefined(); +}); + +test('joins on cca2 codes case-insensitively', () => { + const rows = transformData([{ country_code: 'fr', sum__num: 1 }], { + ...formData, + countryFieldtype: 'cca2', + }); + expect(rows[0].country).toEqual('FRA'); + expect(rows[0].code).toEqual('FR'); +}); + +test('marks unmatched countries as XXX', () => { + const rows = transformData( + [ + { country_code: 'NOT_A_COUNTRY', sum__num: 1 }, + { country_code: null, sum__num: 2 }, + ], + formData, + ); + expect(rows[0].country).toEqual('XXX'); + expect(rows[1].country).toEqual('XXX'); +}); + +test('fills m2 from the secondary metric, or mirrors m1 when equal', () => { + const withSecondary = transformData( + [{ country_code: 'FRA', sum__num: 42, count: 7 }], + { ...formData, secondaryMetric: 'count' }, + ); + expect(withSecondary[0]).toMatchObject({ m1: 42, m2: 7 }); + + const mirrored = transformData([{ country_code: 'FRA', sum__num: 42 }], { + ...formData, + secondaryMetric: 'sum__num', + }); + expect(mirrored[0]).toMatchObject({ m1: 42, m2: 42 }); +}); + +test('joins by full country name', () => { + const rows = transformData([{ country_code: 'France', sum__num: 1 }], { + ...formData, + countryFieldtype: 'name', + }); + expect(rows[0].country).toEqual('FRA'); + expect(rows[0].code).toEqual('France'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts new file mode 100644 index 000000000000..91af7950ff14 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts @@ -0,0 +1,80 @@ +/** + * 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 { supersetTheme } from '@apache-superset/core/theme'; +import { ChartProps } from '@superset-ui/core'; +import transformProps from '../src/transformProps'; + +test('joins country metadata from camelized chart props form data', () => { + const chartProps = new ChartProps({ + width: 800, + height: 600, + datasource: { columnFormats: {}, currencyFormats: {}, verboseMap: {} }, + theme: supersetTheme, + hooks: {}, + filterState: {}, + formData: { + entity: 'country_code', + country_fieldtype: 'cca3', + metric: 'sum__num', + secondary_metric: 'count', + max_bubble_size: '25', + color_picker: { r: 0, g: 122, b: 135, a: 1 }, + show_bubbles: true, + }, + queriesData: [{ data: [{ country_code: 'FRA', sum__num: 42, count: 7 }] }], + }); + const { data } = transformProps(chartProps) as { + data: Record[]; + }; + expect(data[0]).toMatchObject({ + country: 'FRA', + name: 'France', + m1: 42, + m2: 7, + }); +}); + +test('passes through pre-shaped legacy payloads', () => { + const legacyRow = { + country: 'FRA', + m1: 42, + name: 'France', + latitude: 46, + longitude: 2, + }; + const chartProps = new ChartProps({ + width: 800, + height: 600, + datasource: { columnFormats: {}, currencyFormats: {}, verboseMap: {} }, + theme: supersetTheme, + hooks: {}, + filterState: {}, + formData: { + entity: 'country_code', + country_fieldtype: 'cca3', + metric: 'sum__num', + color_picker: { r: 0, g: 122, b: 135, a: 1 }, + }, + queriesData: [{ data: [legacyRow] }], + }); + const { data } = transformProps(chartProps) as { + data: Record[]; + }; + expect(data).toEqual([legacyRow]); +}); From 9729306c06938ac6d8e26852fcece4623f025f83 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 23:02:35 -0700 Subject: [PATCH 11/84] feat(time-table): migrate time_table chart to v1 chart data API (#41723) Co-authored-by: Claude Code --- .../TimeTable/config/buildQuery.test.ts | 57 ++++++++++ .../TimeTable/config/buildQuery.ts | 49 +++++++++ .../transformProps/transformData.test.ts | 95 ++++++++++++++++ .../config/transformProps/transformData.ts | 103 ++++++++++++++++++ .../config/transformProps/transformProps.ts | 22 +++- .../src/visualizations/TimeTable/index.ts | 3 +- 6 files changed, 325 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts create mode 100644 superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts create mode 100644 superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts create mode 100644 superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts diff --git a/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts new file mode 100644 index 000000000000..fcdcd48cb06e --- /dev/null +++ b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.test.ts @@ -0,0 +1,57 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from './buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_grain_sqla: 'P1D', + time_range: 'No filter', + viz_type: 'time_table', + metrics: ['sum__num', 'count'], +}; + +test('builds a timeseries query ordered by the first metric ascending', () => { + const [query] = buildQuery(formData).queries; + expect(query.metrics).toEqual(['sum__num', 'count']); + expect(query.is_timeseries).toBe(true); + expect(query.orderby).toEqual([['sum__num', true]]); + expect(query.granularity).toEqual('ds'); +}); + +test('orders descending when order_desc is set', () => { + const [query] = buildQuery({ ...formData, order_desc: true }).queries; + expect(query.orderby).toEqual([['sum__num', false]]); +}); + +test('keeps the groupby columns for grouped mode', () => { + const [query] = buildQuery({ + ...formData, + metrics: ['sum__num'], + groupby: ['gender'], + }).queries; + expect(query.columns).toEqual(['gender']); +}); + +test('rejects multiple metrics in grouped mode like the legacy backend', () => { + expect(() => buildQuery({ ...formData, groupby: ['gender'] })).toThrow( + 'single metric', + ); +}); diff --git a/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts new file mode 100644 index 000000000000..a5be3d461797 --- /dev/null +++ b/superset-frontend/src/visualizations/TimeTable/config/buildQuery.ts @@ -0,0 +1,49 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { + buildQueryContext, + ensureIsArray, + QueryFormData, +} from '@superset-ui/core'; + +/** + * Mirrors the legacy TimeTableViz.query_obj: a timeseries query ordered + * by the first metric (ascending unless order_desc), limited to a single + * metric when grouped. + */ +export default function buildQuery(formData: QueryFormData) { + const { order_desc, groupby } = formData; + return buildQueryContext(formData, baseQueryObject => { + const metrics = ensureIsArray(baseQueryObject.metrics); + if (ensureIsArray(groupby).length > 0 && metrics.length > 1) { + throw new Error( + t("When using 'Group By' you are limited to use a single metric"), + ); + } + const firstMetric = metrics[0]; + return [ + { + ...baseQueryObject, + is_timeseries: true, + orderby: firstMetric ? [[firstMetric, !order_desc]] : undefined, + }, + ]; + }); +} diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts new file mode 100644 index 000000000000..a01a78c51fa3 --- /dev/null +++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts @@ -0,0 +1,95 @@ +/** + * 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 transformData, { formatTimestamp } from './transformData'; + +const jan1 = 1704067200000; // 2024-01-01T00:00:00Z +const jan2 = 1704153600000; // 2024-01-02T00:00:00Z + +test('formats timestamps like str(pandas.Timestamp)', () => { + expect(formatTimestamp(jan1)).toEqual('2024-01-01 00:00:00'); +}); + +test('pivots metrics into records keyed by time when ungrouped', () => { + const { records, columns, is_group_by } = transformData( + [ + { __timestamp: jan1, sum__num: 1, count: 2 }, + { __timestamp: jan2, sum__num: 3, count: 4 }, + ], + [], + ['sum__num', 'count'], + ); + expect(is_group_by).toBe(false); + expect(columns).toEqual(['count', 'sum__num']); + expect(records).toEqual({ + '2024-01-01 00:00:00': { sum__num: 1, count: 2 }, + '2024-01-02 00:00:00': { sum__num: 3, count: 4 }, + }); +}); + +test('pivots single metric per group value when grouped', () => { + const { records, columns, is_group_by } = transformData( + [ + { __timestamp: jan1, gender: 'boy', sum__num: 1 }, + { __timestamp: jan1, gender: 'girl', sum__num: 2 }, + { __timestamp: jan2, gender: 'boy', sum__num: 3 }, + // girl missing on jan2 -> padded with null + ], + ['gender'], + ['sum__num'], + ); + expect(is_group_by).toBe(true); + expect(columns).toEqual(['boy', 'girl']); + expect(records).toEqual({ + '2024-01-01 00:00:00': { boy: 1, girl: 2 }, + '2024-01-02 00:00:00': { boy: 3, girl: null }, + }); +}); + +test('joins multi-column groups with the flat column separator', () => { + const { columns } = transformData( + [{ __timestamp: jan1, gender: 'boy', state: 'CA, USA', sum__num: 1 }], + ['gender', 'state'], + ['sum__num'], + ); + expect(columns).toEqual(['boy, CA\\, USA']); +}); + +test('escapes bare commas like the backend escape_separator', () => { + const { columns } = transformData( + [{ __timestamp: jan1, a: 'x,y', b: 'z', sum__num: 1 }], + ['a', 'b'], + ['sum__num'], + ); + expect(columns).toEqual(['x\\,y, z']); +}); + +test('preserves sub-second timestamps as distinct keys', () => { + const { records } = transformData( + [ + { __timestamp: jan1, sum__num: 1 }, + { __timestamp: jan1 + 123, sum__num: 2 }, + ], + [], + ['sum__num'], + ); + expect(Object.keys(records).sort()).toEqual([ + '2024-01-01 00:00:00', + '2024-01-01 00:00:00.123000', + ]); +}); diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts new file mode 100644 index 000000000000..2e06d43b04a2 --- /dev/null +++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts @@ -0,0 +1,103 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +const FLAT_COLUMN_SEPARATOR = ', '; + +// escape every bare comma, exactly like the backend escape_separator +const escapeSeparator = (value: string) => value.replace(/,/g, '\\,'); + +const pad = (part: number) => String(part).padStart(2, '0'); + +/** + * Formats an epoch timestamp the way str(pandas.Timestamp) did on the + * legacy endpoint ("YYYY-MM-DD HH:MM:SS", with a microsecond suffix for + * sub-second values), which the TimeTable component re-parses with + * new Date(). + */ +export const formatTimestamp = (value: unknown): string => { + const date = new Date(value as number); + if (Number.isNaN(date.getTime())) { + return String(value); + } + const millis = date.getUTCMilliseconds(); + const fraction = millis > 0 ? `.${String(millis).padStart(3, '0')}000` : ''; + return ( + `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-` + + `${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:` + + `${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}${fraction}` + ); +}; + +export interface TimeTablePayload { + records: Record>; + columns: string[]; + is_group_by: boolean; +} + +/** + * Ports the legacy TimeTableViz.get_data pandas pivot: rows keyed by the + * stringified timestamp, one column per metric, or per group value of the + * single metric when grouped (multi-level group names joined with ", ", + * commas escaped, matching FLAT_COLUMN_SEPARATOR semantics). + */ +export default function transformData( + records: Record[], + groupbyLabels: string[], + metricLabels: string[], +): TimeTablePayload { + const isGroupBy = groupbyLabels.length > 0; + const pivoted: Record> = {}; + const columnSet = new Set(); + + records.forEach(record => { + const timeKey = formatTimestamp(record[DTTM_ALIAS]); + if (!pivoted[timeKey]) { + pivoted[timeKey] = {}; + } + if (isGroupBy) { + const column = + groupbyLabels.length === 1 + ? String(record[groupbyLabels[0]]) + : groupbyLabels + .map(label => escapeSeparator(String(record[label]))) + .join(FLAT_COLUMN_SEPARATOR); + columnSet.add(column); + pivoted[timeKey][column] = record[metricLabels[0]]; + } else { + metricLabels.forEach(metric => { + columnSet.add(metric); + pivoted[timeKey][metric] = record[metric]; + }); + } + }); + + // pandas pivot pads missing combinations with NaN -> null + const columns = Array.from(columnSet).sort(); + Object.values(pivoted).forEach(row => { + columns.forEach(column => { + if (!(column in row)) { + // eslint-disable-next-line no-param-reassign + row[column] = null; + } + }); + }); + + return { records: pivoted, columns, is_group_by: isGroupBy }; +} diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts index d4d6d8e51f2c..b433264384c5 100644 --- a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts +++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformProps.ts @@ -16,7 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps, DataRecord, Metric } from '@superset-ui/core'; +import { + ChartProps, + DataRecord, + ensureIsArray, + getColumnLabel, + getMetricLabel, + Metric, + QueryFormMetric, +} from '@superset-ui/core'; +import transformData from './transformData'; interface FormData { groupby: string[]; @@ -44,7 +53,16 @@ interface ColumnData { export function transformProps(chartProps: TableChartProps) { const { height, datasource, formData, queriesData } = chartProps; const { columnCollection = [], groupby, metrics, url } = formData; - const { records, columns } = queriesData[0].data; + // v1 chart data responses arrive as flat records; the legacy explore_json + // endpoint delivered a pre-pivoted {records, columns} payload. + const rawData = queriesData[0].data; + const { records, columns } = Array.isArray(rawData) + ? transformData( + rawData as DataRecord[], + ensureIsArray(groupby).map(getColumnLabel), + ensureIsArray(metrics as QueryFormMetric[]).map(getMetricLabel), + ) + : rawData; const isGroupBy = groupby?.length > 0; let rows; diff --git a/superset-frontend/src/visualizations/TimeTable/index.ts b/superset-frontend/src/visualizations/TimeTable/index.ts index 3dcf0f3cb4f9..07a7023c8a95 100644 --- a/superset-frontend/src/visualizations/TimeTable/index.ts +++ b/superset-frontend/src/visualizations/TimeTable/index.ts @@ -34,7 +34,6 @@ const metadata = new ChartMetadata({ tags: [ t('Multi-Variables'), t('Comparison'), - t('Legacy'), t('Percentages'), t('Tabular'), t('Text'), @@ -42,7 +41,6 @@ const metadata = new ChartMetadata({ ], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class TimeTableChartPlugin extends ChartPlugin { @@ -50,6 +48,7 @@ export default class TimeTableChartPlugin extends ChartPlugin { super({ metadata, transformProps, + loadBuildQuery: () => import('./config/buildQuery'), loadChart: () => import('./TimeTable'), controlPanel, }); From e61610bd98bbecd8c2729d8e641086b6f76e47d6 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 2 Jul 2026 23:08:22 -0700 Subject: [PATCH 12/84] chore(viz): update SCOPE tracker after tier 1-2 merges Co-Authored-By: Claude Fable 5 --- SCOPE.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index b84d20c69121..0b0c909be6b3 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -40,14 +40,14 @@ this branch is the EOL. | 1 | `para` | legacy-plugin-chart-parallel-coordinates | Convert in place (pass-through) | ✅ (merged) | | 2 | `country_map` | legacy-plugin-chart-country-map | Convert in place (select/rename) | ✅ (merged) | | 3 | `bullet` | legacy-preset-chart-nvd3/Bullet | Convert in place (single metric) | ✅ (merged) | -| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | 🔃 (PR open) | -| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | 🔃 (PR open) | -| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | 🔃 (PR open) | -| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | 🔃 (PR open) | -| 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ⬜ | -| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ⬜ | -| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ⬜ | -| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | ⬜ | +| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | ✅ (merged) | +| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | ✅ (merged) | +| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | ✅ (merged) | +| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | ✅ (merged) | +| 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ✅ (merged) | +| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | 🔃 (#41725) | +| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | 🔃 (#41726) | +| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | 🔃 (#41727) | | 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | ⬜ | | 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | ⬜ | | 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | ⬜ | @@ -100,8 +100,14 @@ Per-chart checklist: country_id/metric rename in transformProps with both-labels guard, 15 tests. - 2026-07-02 — #41718 `bullet` merged: buildQuery (single ungrouped metric), records→{measures} reshape in nvd3 transformProps Bullet branch, 5 tests. -- Open PRs: #41719 `chord`, #41720 `world_map`, #41721 `paired_ttest`, - #41723 `time_table`. +- 2026-07-02 — #41719 `chord`, #41720 `world_map`, #41721 `paired_ttest`, + #41723 `time_table`, #41724 `cal_heatmap` merged. Notable bot-caught fixes + along the way: ChartProps camelization bug in world_map's country join, + numeric group ordering in paired_ttest, comma escaping + sub-second keys + + grouped single-metric guard in time_table. +- Open PRs: #41725 `horizon`, #41726 `rose` (uses server-side post_processing + pipeline: pivot/resample/rolling/compare/contribution/flatten), + #41727 `time_pivot` (pandas offset rollback ported to JS). - 2026-07-02 — Phase 0: removed orphaned `viz.py` classes (`MapboxViz`, `MapLibreViz`, `EventFlowViz`, −204 lines) and the unregistered nvd3 BoxPlot story leftovers. From 0fea3c88a5f77d4032f8a45c5650ba303d7c425f Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 23:19:33 -0700 Subject: [PATCH 13/84] feat(horizon): migrate horizon chart to v1 chart data API (#41725) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 33 ++++++ .../legacy-plugin-chart-horizon/src/index.ts | 4 +- .../src/transformData.ts | Bin 0 -> 4607 bytes .../src/transformProps.ts | 28 ++++- .../test/buildQuery.test.ts | 40 +++++++ .../test/transformData.test.ts | 100 ++++++++++++++++++ 6 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts new file mode 100644 index 000000000000..dc195ef0e66f --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts @@ -0,0 +1,33 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * Mirrors the legacy HorizonViz query: a plain timeseries query grouped + * by the dimensions. Series limiting (the `limit` control) and + * order_desc are handled by buildQueryObject. + */ +export default function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + is_timeseries: true, + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts index ea5a357c4b42..034e9775d737 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts @@ -33,16 +33,16 @@ const metadata = new ChartMetadata({ ), exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('Horizon Chart'), - tags: [t('Legacy')], + tags: [], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class HorizonChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./HorizonChart'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformData.ts new file mode 100644 index 0000000000000000000000000000000000000000..7089e13daad5517c36ffd9d7703047c468b82d00 GIT binary patch literal 4607 zcmZ`+O>^7G5zSe@qNT$Mup!V+Zj>oSv8)ZVHkC5D-r7>Bwua=8L<9l|W?-83>c8jp z^b7z>%0(hD)7`JTUw>fm>eUgwqL0}^7e+71mSi`Yt}3;_ziGL$J5_7CDVt)cY*rQ& zT}^Ld!lSOKEHq1(HEm1WT9k#Yvw36jA{ULM*0t7KU09P6O|^FFC%3cl`wz6rawosc zjK~4NF0&il$qenv`hiwZrIyQ#O{<);0xz}>2pg{TTGh*}Si^L+ue0^WQn@R1Z8lj2 zdoy-rdK17HQ5?*|*L~S|S9)IiU?lVfirD49rf(?11vYu&Pfy}=C%sjBDoRU@Vb=hg zwUhp~(3J&T0NYl1riz7=_TK3baA5o$5Lih$w+i=_bA`&43zRoT%EC6AC)!wB4F`kW zZkH;TrL?Tq10QZP_=vzxrXS9L*^B&Ic34^Bf|4NsS21DY7V5i+Hw9|nq2yU zVOJyG#X4bwCQzWCB*{L9EC7V#{R;@1Dp>sFYD(kj3H{@0I-Vx<+j#cR+h1q&+tugK zSCiTJ!<246)BD@W^>{YEo#5$)t|q^;#XH7O(pb}2ckB$JD1+v2GdZiW!709~SPj#JXLqCp) z9`t@l%zS|Q)Bm`;KgY>el{eZ9VeQ)xfF25v8gv^{(QM}k$DjC-=k&<8ACHa(QpEp; zJ*GI4>P*q(%k|%8*;b3qFWGmhvajIkYn1sPMt~r!8&zrMFH?gfz#2AXoqb0Z7aLXE zAs0G%0eDK$Rs;4Tp>kOt!% zN~o-Sp|`CB3K&yVm%B5VMw_xoXN;OdAIP)sE^!H|JU??W@hF#dSyg(OIA;Vct0~l0 zlRuN>v9n$V`JmlK18|X}9^tYG63A_It{0XSf-W?u?#d-O5_m72^utFZ zTB+RV7004Z_3jeNxra1l-+UM9@z(82EJK zKAl~3vEm0;tLME7qd=S2{qks03UR);3V*>iFp#H4ri-<bUXnvKSAqGATKvu+FDy}s)d4fUug_kFM-B-Dtp2y z{qz$ZPu$LsW@e%$k-KxiOY%rxdsBf$9gjL@wGng#ZsB0cva^Rw==!qTA$aIL8$Z&7 zLTDV~esuw2CXrJvaS`X+O$Knm=qbQ3BmZtaklCfP{M2TNRMjbJY>us%v zomELLw+WvZ?o;i(xGkqkl31H3nR{M8tq8Aoc^=}=9Q28WwI@kE$Z5Yl5;3&Cp3n_g z-=2=;Fc6yQb`Th_zztSD5OTd#2XZR03q0?*!5yZH2wh%wHNb)l&{#d=Q&>z;PYlDzhV%DCGiVO-*#ajz zATRE#LUSd%fo#T^asMNga+3jr-H-G?_?n4M|8g3G%ZkV?u{s5 eHj8~^o%Q^mz1iJ)=WYPg;g;Mk*R8kw>G(gS>A+_I literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts index cab69e8c5683..785e6782741e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts @@ -16,21 +16,45 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getColumnLabel, + getMetricLabel, + QueryFormMetric, +} from '@superset-ui/core'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { height, width, formData, queriesData } = chartProps; const { horizon_color_scale: horizonColorScale, series_height: seriesHeight, + groupby, + metrics, + contribution, } = formData; + // v1 responses arrive as flat records; the legacy explore_json endpoint + // delivered pre-pivoted series. + const rawData = queriesData[0].data; + const data = Array.isArray(rawData) + ? transformData( + rawData, + ensureIsArray(groupby).map(getColumnLabel), + ensureIsArray(metrics as QueryFormMetric[]).map(getMetricLabel), + Boolean(contribution), + ) + : // legacy pre-shaped payloads pass through; nullish becomes an + // empty series list so the renderer's data.map is safe + (rawData ?? []); + // Only include colorScale if defined, otherwise let defaultProps apply return { ...(horizonColorScale !== undefined && { colorScale: horizonColorScale as string, }), - data: queriesData[0].data, + data, height, seriesHeight: parseInt(String(seriesHeight ?? 20), 10), width, diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts new file mode 100644 index 000000000000..52dcc84bade7 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts @@ -0,0 +1,40 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_grain_sqla: 'P1D', + time_range: 'No filter', + viz_type: 'horizon', + groupby: ['gender'], + metrics: ['sum__num'], + limit: 25, +}; + +test('builds a grouped timeseries query with a series limit', () => { + const [query] = buildQuery(formData).queries; + expect(query.columns).toEqual(['gender']); + expect(query.metrics).toEqual(['sum__num']); + expect(query.is_timeseries).toBe(true); + expect(query.series_limit).toEqual(25); + expect(query.granularity).toEqual('ds'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts new file mode 100644 index 000000000000..3c91289e0585 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts @@ -0,0 +1,100 @@ +/** + * 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 transformData from '../src/transformData'; + +const t1 = 1704067200000; +const t2 = 1704153600000; + +test('builds one series per group, dropping the single metric from the key', () => { + const data = transformData( + [ + { __timestamp: t1, gender: 'boy', sum__num: 10 }, + { __timestamp: t2, gender: 'boy', sum__num: 20 }, + { __timestamp: t1, gender: 'girl', sum__num: 30 }, + ], + ['gender'], + ['sum__num'], + ); + expect(data).toEqual([ + { + key: ['boy'], + values: [ + { x: t1, y: 10 }, + { x: t2, y: 20 }, + ], + }, + { + key: ['girl'], + values: [ + { x: t1, y: 30 }, + { x: t2, y: null }, + ], + }, + ]); +}); + +test('keys ungrouped series by the metric name', () => { + const data = transformData( + [ + { __timestamp: t1, sum__num: 1, count: 2 }, + { __timestamp: t2, sum__num: 3, count: 4 }, + ], + [], + ['sum__num', 'count'], + ); + expect(data.map(series => series.key)).toEqual(['count', 'sum__num']); +}); + +test('keeps the metric in multi-metric grouped keys', () => { + const data = transformData( + [{ __timestamp: t1, gender: 'boy', sum__num: 1, count: 2 }], + ['gender'], + ['sum__num', 'count'], + ); + expect(data.map(series => series.key)).toEqual([ + ['count', 'boy'], + ['sum__num', 'boy'], + ]); +}); + +test('normalizes rows when contribution is on', () => { + const data = transformData( + [ + { __timestamp: t1, gender: 'boy', sum__num: 30 }, + { __timestamp: t1, gender: 'girl', sum__num: 10 }, + ], + ['gender'], + ['sum__num'], + true, + ); + expect(data[0].values[0].y).toBeCloseTo(0.75); + expect(data[1].values[0].y).toBeCloseTo(0.25); +}); + +test('drops series whose values are all null', () => { + const data = transformData( + [ + { __timestamp: t1, gender: 'boy', sum__num: 5 }, + { __timestamp: t1, gender: 'girl', sum__num: null }, + ], + ['gender'], + ['sum__num'], + ); + expect(data.map(series => series.key)).toEqual([['boy']]); +}); From 311b0087f319b7137363da52cc54afbb367b1d10 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 23:35:12 -0700 Subject: [PATCH 14/84] feat(rose): migrate rose chart to v1 chart data API (#41726) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 83 ++++++++++ .../legacy-plugin-chart-rose/src/index.ts | 3 +- .../src/transformData.ts | 142 ++++++++++++++++++ .../src/transformProps.ts | 27 +++- .../test/buildQuery.test.ts | 88 +++++++++++ .../test/transformData.test.ts | 98 ++++++++++++ 6 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts new file mode 100644 index 000000000000..e45103793634 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts @@ -0,0 +1,83 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + QueryFormData, +} from '@superset-ui/core'; +import { + flattenOperator, + isTimeComparison, + pivotOperator, + resampleOperator, + rollingWindowOperator, + timeCompareOperator, + timeComparePivotOperator, +} from '@superset-ui/chart-controls'; +import type { PostProcessingContribution } from '@superset-ui/core'; + +/** The legacy `contribution` checkbox row-normalized the pivoted frame. */ +const legacyContributionOperator = ( + formData: QueryFormData, +): PostProcessingContribution | undefined => + formData.contribution + ? { operation: 'contribution', options: { orientation: 'row' } } + : undefined; + +/** + * Mirrors the legacy NVD3TimeSeriesViz pipeline the rose chart rode on: + * pivot on the timestamp, then resample, rolling window, time compare + * and contribution as post-processing, flattened back into records for + * transformProps to reshape. + */ +export default function buildQuery(rawFormData: QueryFormData) { + // the legacy control emits 'absolute' where the v1 compare + // post-processing operation expects 'difference' + const formData: QueryFormData = { + ...rawFormData, + comparison_type: + rawFormData.comparison_type === 'absolute' + ? 'difference' + : rawFormData.comparison_type, + }; + return buildQueryContext(formData, baseQueryObject => { + const queryObject = { + ...baseQueryObject, + is_timeseries: true, + time_offsets: isTimeComparison(formData, baseQueryObject) + ? ensureIsArray(formData.time_compare) + : [], + }; + return [ + { + ...queryObject, + post_processing: [ + isTimeComparison(formData, queryObject) + ? timeComparePivotOperator(formData, queryObject) + : pivotOperator(formData, queryObject), + resampleOperator(formData, queryObject), + rollingWindowOperator(formData, queryObject), + timeCompareOperator(formData, queryObject), + legacyContributionOperator(formData), + flattenOperator(formData, queryObject), + ], + }, + ]; + }); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts index 3fcf289f8a8a..ad6bc2e7e689 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts @@ -38,7 +38,6 @@ const metadata = new ChartMetadata({ ], name: t('Nightingale Rose Chart'), tags: [ - t('Legacy'), t('Advanced-Analytics'), t('Circular'), t('Multi-Layers'), @@ -48,13 +47,13 @@ const metadata = new ChartMetadata({ ], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class RoseChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactRose'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts new file mode 100644 index 000000000000..862ba856ee95 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts @@ -0,0 +1,142 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +export interface RoseEntry { + key: string | string[]; + value: number; + name: string | string[]; + time: unknown; +} + +export type RoseData = Record; + +/** Splits a flattened pivot column label on ", " honoring "\," escapes. */ +export const splitFlatLabel = (label: string): string[] => + label.split(/(? part.replace(/\\,/g, ',')); + +interface SeriesKeyOptions { + metricLabels: string[]; + timeCompare: string[]; + comparisonType?: string; +} + +/** + * Recovers the legacy to_series key for a flattened pivot column: + * the metric is dropped from grouped single-metric keys, time-shifted + * columns get the legacy " offset" suffix, and comparison + * columns (difference__m__m__offset) collapse to the same suffix form. + */ +export const seriesKeyOf = ( + parts: string[], + { metricLabels, timeCompare, comparisonType }: SeriesKeyOptions, +): string | string[] | null => { + const [head, ...groups] = parts; + const hasGroup = groups.length > 0; + const singleMetric = metricLabels.length === 1; + + const baseKey = (metric: string): string | string[] => { + if (!hasGroup) { + return metric; + } + return singleMetric ? groups : [metric, ...groups]; + }; + + if (comparisonType && comparisonType !== 'values') { + // head looks like `${comparisonType}__${metric}__${metric}__${offset}` + const prefix = `${comparisonType}__`; + if (!head.startsWith(prefix)) { + return null; // originals are dropped in comparison mode + } + const rest = head.slice(prefix.length); + const metric = metricLabels.find(label => + rest.startsWith(`${label}__${label}__`), + ); + if (!metric) { + return null; + } + const offset = rest.slice(metric.length * 2 + 4); + const base = baseKey(metric); + const suffix = `${offset} offset`; + return Array.isArray(base) ? [...base, suffix] : [base, suffix]; + } + + // values mode: time-shifted columns are named `${metric}__${offset}` + for (const metric of metricLabels) { + for (const offset of timeCompare) { + if (head === `${metric}__${offset}`) { + const base = baseKey(metric); + const suffix = `${offset} offset`; + return Array.isArray(base) ? [...base, suffix] : [base, suffix]; + } + } + } + return baseKey(head); +}; + +/** + * Ports the legacy RoseViz.get_data reshape: the pivoted series are + * re-keyed by timestamp, one entry per series with NaN collapsed to 0. + */ +export default function transformData( + records: Record[], + options: SeriesKeyOptions, +): RoseData { + const columns = new Set(); + records.forEach(record => { + Object.keys(record).forEach(column => { + if (column !== DTTM_ALIAS) { + columns.add(column); + } + }); + }); + + const series: { key: string | string[]; column: string }[] = []; + columns.forEach(column => { + const key = seriesKeyOf(splitFlatLabel(column), options); + if (key != null) { + series.push({ key, column }); + } + }); + // legacy sort_series=False sorts the series by key + series.sort((a, b) => { + const aKey = Array.isArray(a.key) ? a.key.join(' ') : a.key; + const bKey = Array.isArray(b.key) ? b.key.join(' ') : b.key; + if (aKey === bKey) return 0; + return aKey < bKey ? -1 : 1; + }); + + const sorted = [...records].sort((a, b) => { + const aTime = a[DTTM_ALIAS] as number; + const bTime = b[DTTM_ALIAS] as number; + return aTime === bTime ? 0 : aTime < bTime ? -1 : 1; + }); + + const result: RoseData = {}; + sorted.forEach(record => { + const timestamp = record[DTTM_ALIAS] as number; + const entries: RoseEntry[] = series.map(({ key, column }) => { + const raw = record[column]; + const value = typeof raw === 'number' && !Number.isNaN(raw) ? raw : 0; + return { key, value, name: key, time: timestamp }; + }); + result[String(timestamp)] = entries; + }); + return result; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts index b37e26a8722c..1cf5aaf90d63 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts @@ -16,7 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getMetricLabel, + QueryFormMetric, +} from '@superset-ui/core'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; @@ -27,12 +33,29 @@ export default function transformProps(chartProps: ChartProps) { richTooltip, roseAreaProportion, sliceId, + metrics, + timeCompare, + comparisonType, } = formData; + // v1 responses arrive as flattened pivot records; the legacy + // explore_json endpoint delivered the timestamp-keyed shape directly. + const rawData = queriesData[0].data; + const data = Array.isArray(rawData) + ? transformData(rawData, { + metricLabels: ensureIsArray(metrics as QueryFormMetric[]).map( + getMetricLabel, + ), + timeCompare: ensureIsArray(timeCompare), + comparisonType: + comparisonType === 'absolute' ? 'difference' : comparisonType, + }) + : rawData; + return { width, height, - data: queriesData[0].data, + data, colorScheme, dateTimeFormat, numberFormat, diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts new file mode 100644 index 000000000000..25e8e3e77e53 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts @@ -0,0 +1,88 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_grain_sqla: 'P1D', + time_range: '2023 : 2024', + viz_type: 'rose', + groupby: ['gender'], + metrics: ['sum__num'], +}; + +test('builds a pivot + flatten post-processing pipeline', () => { + const [query] = buildQuery(formData).queries; + const operations = (query.post_processing || []) + .filter(Boolean) + .map(op => op!.operation); + expect(operations).toEqual(['pivot', 'flatten']); + expect(query.is_timeseries).toBe(true); + expect(query.time_offsets).toEqual([]); +}); + +test('adds rolling, resample and contribution operators when configured', () => { + const [query] = buildQuery({ + ...formData, + rolling_type: 'mean', + rolling_periods: 7, + resample_rule: '1D', + resample_method: 'ffill', + contribution: true, + }).queries; + const operations = (query.post_processing || []) + .filter(Boolean) + .map(op => op!.operation); + expect(operations).toEqual([ + 'pivot', + 'resample', + 'rolling', + 'contribution', + 'flatten', + ]); +}); + +test('normalizes the legacy absolute comparison type to difference', () => { + const [query] = buildQuery({ + ...formData, + time_compare: ['1 week ago'], + comparison_type: 'absolute', + }).queries; + const compare = (query.post_processing || []) + .filter(Boolean) + .find(op => op!.operation === 'compare'); + expect((compare?.options as { compare_type?: string })?.compare_type).toEqual( + 'difference', + ); +}); + +test('requests time offsets and compares when time_compare is set', () => { + const [query] = buildQuery({ + ...formData, + time_compare: ['1 week ago'], + comparison_type: 'difference', + }).queries; + expect(query.time_offsets).toEqual(['1 week ago']); + const operations = (query.post_processing || []) + .filter(Boolean) + .map(op => op!.operation); + expect(operations).toContain('compare'); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts new file mode 100644 index 000000000000..22ba3d78ffa1 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts @@ -0,0 +1,98 @@ +/** + * 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 transformData, { + seriesKeyOf, + splitFlatLabel, +} from '../src/transformData'; + +const t1 = 1704067200000; +const t2 = 1704153600000; + +test('re-keys flattened pivot records by timestamp with NaN as 0', () => { + const data = transformData( + [ + { __timestamp: t1, 'sum__num, boy': 10, 'sum__num, girl': 20 }, + { __timestamp: t2, 'sum__num, boy': 30, 'sum__num, girl': null }, + ], + { metricLabels: ['sum__num'], timeCompare: [] }, + ); + expect(data[String(t1)]).toEqual([ + { key: ['boy'], value: 10, name: ['boy'], time: t1 }, + { key: ['girl'], value: 20, name: ['girl'], time: t1 }, + ]); + expect(data[String(t2)][1]).toEqual({ + key: ['girl'], + value: 0, + name: ['girl'], + time: t2, + }); +}); + +test('keys ungrouped series by the metric name', () => { + const data = transformData([{ __timestamp: t1, sum__num: 5 }], { + metricLabels: ['sum__num'], + timeCompare: [], + }); + expect(data[String(t1)]).toEqual([ + { key: 'sum__num', value: 5, name: 'sum__num', time: t1 }, + ]); +}); + +test('splits flattened labels honoring escaped commas', () => { + expect(splitFlatLabel('sum__num, CA\\, USA')).toEqual([ + 'sum__num', + 'CA, USA', + ]); +}); + +test('labels time-shifted columns with the legacy offset suffix', () => { + expect( + seriesKeyOf(['sum__num__1 week ago', 'boy'], { + metricLabels: ['sum__num'], + timeCompare: ['1 week ago'], + }), + ).toEqual(['boy', '1 week ago offset']); + expect( + seriesKeyOf(['sum__num__1 week ago'], { + metricLabels: ['sum__num'], + timeCompare: ['1 week ago'], + }), + ).toEqual(['sum__num', '1 week ago offset']); +}); + +test('maps comparison columns and drops originals', () => { + const options = { + metricLabels: ['sum__num'], + timeCompare: ['1 week ago'], + comparisonType: 'difference', + }; + expect( + seriesKeyOf(['difference__sum__num__sum__num__1 week ago', 'boy'], options), + ).toEqual(['boy', '1 week ago offset']); + expect(seriesKeyOf(['sum__num', 'boy'], options)).toBeNull(); +}); + +test('keeps the metric in multi-metric grouped keys', () => { + expect( + seriesKeyOf(['count', 'boy'], { + metricLabels: ['count', 'sum__num'], + timeCompare: [], + }), + ).toEqual(['count', 'boy']); +}); From 3abbe1c84a6e2aab4d0434252cdc9b896d19595f Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Thu, 2 Jul 2026 23:42:41 -0700 Subject: [PATCH 15/84] feat(time-pivot): migrate time_pivot chart to v1 chart data API (#41727) Co-authored-by: Claude Code --- .../src/TimePivot/buildQuery.ts | 34 ++++ .../src/TimePivot/index.ts | 6 +- .../src/TimePivot/transformData.ts | 179 ++++++++++++++++++ .../src/TimePivot/transformProps.ts | 39 ++++ .../test/TimePivot.test.ts | 95 ++++++++++ 5 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts create mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts new file mode 100644 index 000000000000..76a788b6e694 --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts @@ -0,0 +1,34 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * Mirrors the legacy NVD3TimePivotViz.query_obj: a timeseries query with + * the single metric. + */ +export default function buildQuery(formData: QueryFormData) { + const { metric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + is_timeseries: true, + metrics: metric ? [metric] : [], + }, + ]); +} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts index bf98ec9a44eb..f903849837f6 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts @@ -18,7 +18,7 @@ */ import { t } from '@apache-superset/core/translation'; import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from '../transformProps'; +import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; import thumbnailDark from './images/thumbnail-dark.png'; import example from './images/example.jpg'; @@ -33,16 +33,16 @@ const metadata = new ChartMetadata({ ), exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('Time-series Period Pivot'), - tags: [t('Legacy'), t('Time'), t('nvd3')], + tags: [t('Time'), t('nvd3')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class TimePivotChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('../ReactNVD3'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts new file mode 100644 index 000000000000..4d57ecddc2cb --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts @@ -0,0 +1,179 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +const DAY_MS = 24 * 60 * 60 * 1000; +const WEEKDAYS = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; + +/** + * Rolls a timestamp back to the start of its period the way pandas + * offset.rollback(normalize=True) does for the offset aliases the + * frequency control offers (AS/A, QS/Q, MS/M, W and W-XXX, D, H, T/MIN). + * The multiplier prefix (e.g. 52W-MON) does not change the anchor. + */ +export const rollback = (timestamp: number, freq: string): number => { + const match = /^\d*(AS|YS|A|Y|QS|Q|MS|M|W(?:-([A-Z]{3}))?|D|H|T|MIN)$/i.exec( + (freq || 'W-MON').trim(), + ); + const date = new Date(timestamp); + const midnight = Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + ); + if (!match) { + return midnight; // freeform rules fall back to day boundaries + } + const unit = match[1].toUpperCase(); + if (unit === 'AS' || unit === 'YS') { + return Date.UTC(date.getUTCFullYear(), 0, 1); + } + if (unit === 'A' || unit === 'Y') { + // most recent Dec 31 at or before the timestamp + const yearEnd = Date.UTC(date.getUTCFullYear(), 11, 31); + return midnight >= yearEnd + ? yearEnd + : Date.UTC(date.getUTCFullYear() - 1, 11, 31); + } + if (unit === 'QS') { + return Date.UTC( + date.getUTCFullYear(), + Math.floor(date.getUTCMonth() / 3) * 3, + 1, + ); + } + if (unit === 'Q') { + // most recent quarter end at or before the timestamp + const quarterEndMonth = Math.floor(date.getUTCMonth() / 3) * 3 + 3; + const quarterEnd = Date.UTC(date.getUTCFullYear(), quarterEndMonth, 0); + return midnight >= quarterEnd + ? quarterEnd + : Date.UTC(date.getUTCFullYear(), quarterEndMonth - 3, 0); + } + if (unit === 'MS') { + return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1); + } + if (unit === 'M') { + // most recent month end at or before the timestamp + const monthEnd = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0); + return midnight >= monthEnd + ? monthEnd + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 0); + } + if (unit.startsWith('W')) { + const anchor = match[2] ? WEEKDAYS.indexOf(match[2].toUpperCase()) : 6; // W defaults to W-SUN + // JS getUTCDay: 0=SUN..6=SAT; WEEKDAYS index: 0=MON..6=SUN + const jsAnchor = anchor === 6 ? 0 : anchor + 1; + const diff = (date.getUTCDay() - jsAnchor + 7) % 7; + return midnight - diff * DAY_MS; + } + if (unit === 'D') { + return midnight; + } + if (unit === 'H') { + return timestamp - (timestamp % (60 * 60 * 1000)); + } + return timestamp - (timestamp % (60 * 1000)); // T / MIN +}; + +export interface TimePivotSeries { + key: string; + values: { x: number; y: number | null }[]; + rank: number; + perc: number; +} + +/** + * Ports the legacy NVD3TimePivotViz.get_data reshape: timestamps are + * bucketed into periods by the freq offset, ranked most-recent-first + * ("current", "-1", "-2", ...), shifted onto the latest period's time + * axis, and pivoted into one series per period with rank/perc metadata. + */ +export default function transformData( + records: Record[], + metricLabel: string, + freq: string, +): TimePivotSeries[] { + const rows = records + .filter(record => record[DTTM_ALIAS] != null) + .map(record => { + const timestamp = record[DTTM_ALIAS] as number; + return { + timestamp, + period: rollback(timestamp, freq), + value: record[metricLabel] as number | null, + }; + }); + if (rows.length === 0) { + return []; + } + + const periods = Array.from(new Set(rows.map(row => row.period))).sort( + (a, b) => b - a, + ); + const maxPeriod = periods[0]; + const rankOf = new Map(periods.map((period, index) => [period, index])); + const maxRank = periods.length - 1; + + const seriesOf = (rank: number) => (rank === 0 ? 'current' : `-${rank}`); + + // shift every point onto the latest period's time axis + const values = new Map>(); + const shiftedTimestamps = new Set(); + rows.forEach(({ timestamp, period, value }) => { + const series = seriesOf(rankOf.get(period)!); + const shifted = timestamp + (maxPeriod - period); + shiftedTimestamps.add(shifted); + if (!values.has(series)) { + values.set(series, new Map()); + } + values.get(series)!.set(shifted, value); + }); + + const timestamps = Array.from(shiftedTimestamps).sort((a, b) => a - b); + // pandas pivot sorts the series labels lexicographically + const seriesLabels = Array.from(values.keys()).sort(); + + const chartData: TimePivotSeries[] = []; + seriesLabels.forEach(series => { + const seriesValues = values.get(series)!; + let nonNullCount = 0; + const points = timestamps.map(timestamp => { + let y = seriesValues.has(timestamp) ? seriesValues.get(timestamp)! : null; + if (typeof y === 'number' && Number.isNaN(y)) { + y = null; + } + if (y != null) { + nonNullCount += 1; + } + return { x: timestamp, y }; + }); + if (nonNullCount === 0) { + return; + } + const rank = series === 'current' ? 0 : Number(series.slice(1)); + chartData.push({ + key: series, + values: points, + rank, + perc: 1 - rank / (maxRank + 1), + }); + }); + return chartData; +} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts new file mode 100644 index 000000000000..62739c13f5de --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts @@ -0,0 +1,39 @@ +/** + * 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 { ChartProps, getMetricLabel } from '@superset-ui/core'; +import nvd3TransformProps from '../transformProps'; +import transformData from './transformData'; + +export default function transformProps(chartProps: ChartProps) { + const { formData, queriesData } = chartProps; + const rawData = queriesData[0].data; + // v1 responses arrive as flat records; the legacy explore_json endpoint + // delivered the period-pivoted series directly. + const data = Array.isArray(rawData) + ? transformData( + rawData as Record[], + getMetricLabel(formData.metric), + formData.freq as string, + ) + : rawData; + return nvd3TransformProps({ + ...chartProps, + queriesData: [{ ...queriesData[0], data }], + } as ChartProps); +} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts new file mode 100644 index 000000000000..7e2f6ec87e20 --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts @@ -0,0 +1,95 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/TimePivot/buildQuery'; +import transformData, { rollback } from '../src/TimePivot/transformData'; + +test('buildQuery builds a timeseries query with the single metric', () => { + const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'Last quarter', + viz_type: 'time_pivot', + metric: 'sum__num', + freq: 'W-MON', + }; + const [query] = buildQuery(formData).queries; + expect(query.metrics).toEqual(['sum__num']); + expect(query.is_timeseries).toBe(true); +}); + +describe('TimePivot rollback', () => { + const wed = Date.UTC(2024, 0, 10, 15, 30); // Wednesday 2024-01-10 + test.each([ + ['W-MON', Date.UTC(2024, 0, 8)], + ['52W-MON', Date.UTC(2024, 0, 8)], + ['W-SUN', Date.UTC(2024, 0, 7)], + ['D', Date.UTC(2024, 0, 10)], + ['AS', Date.UTC(2024, 0, 1)], + ['QS', Date.UTC(2024, 0, 1)], + ['MS', Date.UTC(2024, 0, 1)], + ])('rolls back to the %s period start', (freq, expected) => { + expect(rollback(wed, freq as string)).toEqual(expected); + }); + + test('rolls back to the most recent month end for M', () => { + expect(rollback(wed, 'M')).toEqual(Date.UTC(2023, 11, 31)); + expect(rollback(Date.UTC(2024, 0, 31), 'M')).toEqual(Date.UTC(2024, 0, 31)); + }); +}); + +describe('TimePivot transformData', () => { + const mon1 = Date.UTC(2024, 0, 1); // Monday + const tue1 = Date.UTC(2024, 0, 2); + const mon2 = Date.UTC(2024, 0, 8); // next Monday + const tue2 = Date.UTC(2024, 0, 9); + + test('pivots periods onto the latest period axis with ranks', () => { + const data = transformData( + [ + { __timestamp: mon1, sum__num: 1 }, + { __timestamp: tue1, sum__num: 2 }, + { __timestamp: mon2, sum__num: 3 }, + { __timestamp: tue2, sum__num: 4 }, + ], + 'sum__num', + 'W-MON', + ); + expect(data.map(series => series.key)).toEqual(['-1', 'current']); + const previous = data[0]; + const current = data[1]; + expect(previous.rank).toEqual(1); + expect(previous.perc).toEqual(0.5); + expect(current.rank).toEqual(0); + expect(current.perc).toEqual(1); + // the older week is shifted onto the current week's timestamps + expect(previous.values).toEqual([ + { x: mon2, y: 1 }, + { x: tue2, y: 2 }, + ]); + expect(current.values).toEqual([ + { x: mon2, y: 3 }, + { x: tue2, y: 4 }, + ]); + }); + + test('returns an empty list for empty input', () => { + expect(transformData([], 'sum__num', 'W-MON')).toEqual([]); + }); +}); From 480fbe0e060345a0722084e8d58f709438c93c66 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 3 Jul 2026 00:01:52 -0700 Subject: [PATCH 16/84] chore(viz): update SCOPE tracker after tier 2 completion Co-Authored-By: Claude Fable 5 --- SCOPE.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index 0b0c909be6b3..8ccf6b6a8e04 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -45,13 +45,13 @@ this branch is the EOL. | 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | ✅ (merged) | | 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | ✅ (merged) | | 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ✅ (merged) | -| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | 🔃 (#41725) | -| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | 🔃 (#41726) | -| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | 🔃 (#41727) | -| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | ⬜ | -| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | ⬜ | -| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | ⬜ | -| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | ⬜ | +| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ✅ (merged) | +| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ✅ (merged) | +| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | ✅ (merged) | +| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | 🔃 (#41728) | +| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | 🔃 (staged on #41728) | +| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | 🔃 (#41729) | +| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | 🔃 (#41730) | "Convert in place" = keep the renderer and the `viz_type` key, add a modern `buildQuery.ts`, move the `viz.py` `get_data()` reshape into `transformProps.ts`, @@ -105,9 +105,19 @@ Per-chart checklist: along the way: ChartProps camelization bug in world_map's country join, numeric group ordering in paired_ttest, comma escaping + sub-second keys + grouped single-metric guard in time_table. -- Open PRs: #41725 `horizon`, #41726 `rose` (uses server-side post_processing - pipeline: pivot/resample/rolling/compare/contribution/flatten), - #41727 `time_pivot` (pandas offset rollback ported to JS). +- 2026-07-03 — #41725 `horizon`, #41726 `rose` (server-side post_processing + pipeline; bot caught the legacy 'absolute'→'difference' comparison-type + mismatch), #41727 `time_pivot` (pandas offset rollback ported to JS, incl. + quarters) merged. All 11 convert-in-place charts are migrated. +- Open PRs: #41728 `bubble` retarget (adds the never-shipped Alembic revision + for MigrateBubbleChart; first attempt chained off the wrong alembic head — + filename timestamps lie), #41729 `partition` (full hierarchy port incl. + period analysis), #41730 `deck_multi` (fully client-side; container issues + an empty-queries context, layers fetched via their own buildQuery + + /api/v1/chart/data), #41732 horizon/rose default-ordering parity fix. + `compare` → echarts_timeseries_line is staged on the bubble branch + (migration revision chains after bubble's) and its PR opens when #41728 + merges. - 2026-07-02 — Phase 0: removed orphaned `viz.py` classes (`MapboxViz`, `MapLibreViz`, `EventFlowViz`, −204 lines) and the unregistered nvd3 BoxPlot story leftovers. From 3bcb003eebedafa05d85f09a05136527b26400c4 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 00:16:06 -0700 Subject: [PATCH 17/84] feat(partition): migrate partition chart to v1 chart data API (#41729) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 60 +++ .../src/index.ts | 4 +- .../src/transformData.ts | 402 ++++++++++++++++++ .../src/transformProps.ts | 32 +- .../test/buildQuery.test.ts | 65 +++ .../test/transformData.test.ts | 150 +++++++ 6 files changed, 709 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts create mode 100644 superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts new file mode 100644 index 000000000000..e4df8a1637d7 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts @@ -0,0 +1,60 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + getMetricLabel, + QueryFormData, + QueryFormMetric, + QueryFormOrderBy, +} from '@superset-ui/core'; + +/** + * Mirrors the legacy PartitionViz.query_obj (via NVD3TimeSeriesViz): a + * query grouped by all the hierarchy levels, timeseries only when the + * time-series option needs per-timestamp data, ordered by the sort + * metric (falling back to the first selected metric) ascending unless + * order_desc. + */ +export default function buildQuery(formData: QueryFormData) { + const { time_series_option, timeseries_limit_metric, order_desc } = formData; + return buildQueryContext(formData, baseQueryObject => { + let metrics: QueryFormMetric[] = ensureIsArray(baseQueryObject.metrics); + const orderby: QueryFormOrderBy[] = []; + const sortByMetric = + ensureIsArray( + timeseries_limit_metric as QueryFormMetric | QueryFormMetric[], + )[0] ?? metrics[0]; + if (sortByMetric) { + const sortByLabel = getMetricLabel(sortByMetric); + if (!metrics.some(metric => getMetricLabel(metric) === sortByLabel)) { + metrics = [...metrics, sortByMetric]; + } + orderby.push([sortByMetric, !order_desc]); + } + return [ + { + ...baseQueryObject, + metrics, + is_timeseries: (time_series_option ?? 'not_time') !== 'not_time', + orderby: orderby.length > 0 ? orderby : undefined, + }, + ]; + }); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts index 0ae654105c59..cb75e56f401f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts @@ -30,16 +30,16 @@ const metadata = new ChartMetadata({ description: t('Compare the same summarized metric across multiple groups.'), exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('Partition Chart'), - tags: [t('Categorical'), t('Comparison'), t('Legacy'), t('Proportional')], + tags: [t('Categorical'), t('Comparison'), t('Proportional')], thumbnail, thumbnailDark, - useLegacyApi: true, }); export default class PartitionChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./ReactPartition'), + loadBuildQuery: () => import('./buildQuery'), metadata, transformProps, controlPanel, diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts new file mode 100644 index 000000000000..92d72395b334 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts @@ -0,0 +1,402 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +export interface PartitionNode { + name: unknown; + val?: number | null; + children: PartitionNode[]; +} + +type Row = Record; + +interface Options { + groupbyLabels: string[]; + metricLabels: string[]; + timeSeriesOption?: string; + rollingType?: string; + rollingPeriods?: number; + minPeriods?: number; + contribution?: boolean; +} + +const numeric = (value: unknown): number | null => + typeof value === 'number' && !Number.isNaN(value) ? value : null; + +const sortedUnique = (values: unknown[]): unknown[] => + Array.from(new Set(values)).sort((a, b) => { + if (a === b) return 0; + if (typeof a === 'number' && typeof b === 'number') return a - b; + return String(a) < String(b) ? -1 : 1; + }); + +/** pandas-style aggregate: sum skips nulls, mean of nothing is null */ +const aggregate = (rows: Row[], metric: string, op: 'sum' | 'mean') => { + let total = 0; + let count = 0; + rows.forEach(row => { + const value = numeric(row[metric]); + if (value != null) { + total += value; + count += 1; + } + }); + if (op === 'mean') { + return count === 0 ? null : total / count; + } + return total; +}; + +/** + * Ports PartitionViz.levels_for + nest_values: aggregate each metric at + * every prefix of the group hierarchy and nest the values top-down. The + * `groups` list may include the timestamp column for the plain + * time-series option, exactly like the backend prepended DTTM_ALIAS. + */ +function nestAggregates( + records: Row[], + groups: string[], + metricLabels: string[], + op: 'sum' | 'mean', +): PartitionNode[] { + const nest = ( + metric: string, + rows: Row[], + level: number, + dims: unknown[], + ): PartitionNode[] => { + if (level > groups.length - 1) { + return []; + } + const column = groups[level]; + return sortedUnique(rows.map(row => row[column])).map(value => { + const subset = rows.filter(row => row[column] === value); + return { + // deeper levels carry the dim path in the name, like the backend + name: level === 0 ? value : [...dims, value], + val: aggregate(subset, metric, op), + children: nest(metric, subset, level + 1, [...dims, value]), + }; + }); + }; + return metricLabels.map(metric => ({ + name: metric, + val: aggregate(records, metric, op), + children: nest(metric, records, 0, []), + })); +} + +/** + * Ports PartitionViz.levels_for_diff + nest_values: compare the last + * time grain against the first one at every hierarchy prefix. + */ +function nestPointComparison( + records: Row[], + groups: string[], + metricLabels: string[], + timeOp: 'point_diff' | 'point_factor' | 'point_percent', +): PartitionNode[] { + const times = sortedUnique(records.map(row => row[DTTM_ALIAS])) as number[]; + const since = times[0]; + const until = times[times.length - 1]; + + // top-level comparison has no fill: a plain a-b / a/b / a/b-1 + const topCompare = (a: number, b: number) => { + if (timeOp === 'point_diff') return a - b; + if (timeOp === 'point_factor') return a / b; + return a / b - 1; + }; + // grouped comparisons treat missing sides as 0 (pandas fill_value=0) + const groupedCompare = (a: number | null, b: number | null) => { + const u = a ?? 0; + const s = b ?? 0; + if (timeOp === 'point_diff') return u - s; + if (timeOp === 'point_factor') return u / s; + return u / s - 1; + }; + + const nest = ( + metric: string, + rows: Row[], + level: number, + dims: unknown[], + ): PartitionNode[] => { + if (level > groups.length - 1) { + return []; + } + const column = groups[level]; + return sortedUnique(rows.map(row => row[column])).map(value => { + const subset = rows.filter(row => row[column] === value); + const untilRows = subset.filter(row => row[DTTM_ALIAS] === until); + const sinceRows = subset.filter(row => row[DTTM_ALIAS] === since); + const untilValue = untilRows.length + ? aggregate(untilRows, metric, 'sum') + : null; + const sinceValue = sinceRows.length + ? aggregate(sinceRows, metric, 'sum') + : null; + return { + name: level === 0 ? value : [...dims, value], + val: groupedCompare(untilValue, sinceValue), + children: nest(metric, subset, level + 1, [...dims, value]), + }; + }); + }; + + return metricLabels.map(metric => { + const untilTotal = aggregate( + records.filter(row => row[DTTM_ALIAS] === until), + metric, + 'sum', + ) as number; + const sinceTotal = aggregate( + records.filter(row => row[DTTM_ALIAS] === since), + metric, + 'sum', + ) as number; + return { + name: metric, + val: topCompare(untilTotal, sinceTotal), + children: nest(metric, records, 0, []), + }; + }); +} + +/** apply_rolling equivalent over a time-indexed value list */ +function applyRolling( + values: (number | null)[], + rollingType: string | undefined, + rollingPeriods: number, + minPeriods: number, +): (number | null)[] { + let result = values; + if ( + rollingType && + ['mean', 'sum', 'std'].includes(rollingType) && + rollingPeriods + ) { + result = values.map((_, index) => { + const start = Math.max(0, index - rollingPeriods + 1); + const window = values + .slice(start, index + 1) + .filter((v): v is number => v != null); + if (window.length < Math.max(minPeriods, 1)) { + return null; + } + const sum = window.reduce((acc, v) => acc + v, 0); + if (rollingType === 'sum') return sum; + const mean = sum / window.length; + if (rollingType === 'mean') return mean; + if (window.length < 2) return null; + const variance = + window.reduce((acc, v) => acc + (v - mean) ** 2, 0) / + (window.length - 1); + return Math.sqrt(variance); + }); + } else if (rollingType === 'cumsum') { + let running = 0; + result = values.map(value => { + running += value ?? 0; + return running; + }); + } + return result; +} + +/** + * Ports PartitionViz.levels_for_time + nest_procs for the "period + * analysis" option: pivot each hierarchy prefix per timestamp (sum, + * missing filled with 0), apply the rolling window and contribution the + * way process_data did, and nest as metric -> time -> group levels. + */ +function nestProcs( + records: Row[], + groups: string[], + options: Options, +): PartitionNode[] { + const { + metricLabels, + rollingType, + rollingPeriods = 0, + minPeriods = 0, + contribution, + } = options; + const allTimes = sortedUnique(records.map(row => row[DTTM_ALIAS])); + // rolling windows may trim the leading periods + const times = minPeriods ? allTimes.slice(minPeriods) : allTimes; + + // value series per (level, metric, group tuple) + const seriesFor = ( + metric: string, + rows: Row[], + ): Map => { + const perTime = new Map(); + allTimes.forEach(time => { + const timeRows = rows.filter(row => row[DTTM_ALIAS] === time); + // pivot fill_value=0: missing combinations become 0 + perTime.set( + time, + timeRows.length ? aggregate(timeRows, metric, 'sum') : 0, + ); + }); + let values: (number | null)[] = allTimes.map( + time => perTime.get(time) ?? 0, + ); + values = applyRolling(values, rollingType, rollingPeriods, minPeriods); + const result = new Map(); + allTimes.forEach((time, index) => result.set(time, values[index])); + return result; + }; + + // contribution normalizes each timestamp across every column of the + // same level (all metric x group-tuple combinations) + const buildLevel = ( + level: number, + ): Map> => { + const columns = new Map(); + metricLabels.forEach(metric => { + if (level === 0) { + columns.set(JSON.stringify([metric]), records); + } else { + const tuples = new Map(); + records.forEach(row => { + const key = JSON.stringify([ + metric, + ...groups.slice(0, level).map(label => row[label]), + ]); + const bucket = tuples.get(key); + if (bucket) { + bucket.push(row); + } else { + tuples.set(key, [row]); + } + }); + tuples.forEach((rows, key) => columns.set(key, rows)); + } + }); + const built = new Map>(); + columns.forEach((rows, key) => { + built.set(key, seriesFor(JSON.parse(key)[0], rows)); + }); + if (contribution) { + allTimes.forEach(time => { + let total = 0; + built.forEach(series => { + total += series.get(time) ?? 0; + }); + built.forEach(series => { + const value = series.get(time); + series.set(time, total === 0 ? null : (value ?? 0) / total); + }); + }); + } + return built; + }; + + const levels = groups.map((_, index) => buildLevel(index + 1)); + const level0 = buildLevel(0); + + const nest = ( + metric: string, + level: number, + dims: unknown[], + time: unknown, + ): PartitionNode[] => { + if (level > groups.length) { + return []; + } + const built = levels[level - 1]; + const wanted = [metric, ...dims]; + const children: PartitionNode[] = []; + built.forEach((series, key) => { + const parts = JSON.parse(key) as unknown[]; + if ( + parts.length !== wanted.length + 1 || + !wanted.every((part, index) => parts[index] === part) + ) { + return; + } + const name = parts[parts.length - 1]; + children.push({ + name, + val: series.get(time) ?? null, + children: nest(metric, level + 1, [...dims, name], time), + }); + }); + return children; + }; + + return metricLabels.map(metric => ({ + name: metric, + children: times.map(time => ({ + name: time, + val: level0.get(JSON.stringify([metric]))?.get(time) ?? null, + children: nest(metric, 1, [], time), + })), + })); +} + +/** + * Ports the legacy PartitionViz.get_data dispatch across the + * time-series options. + */ +export default function transformData( + records: Row[], + options: Options, +): PartitionNode[] { + const { + groupbyLabels, + metricLabels, + timeSeriesOption = 'not_time', + } = options; + if (groupbyLabels.length === 0) { + throw new Error('Please choose at least one groupby'); + } + if (records.length === 0) { + return []; // the legacy endpoint returned no data for an empty frame + } + if (timeSeriesOption === 'not_time' || timeSeriesOption === 'agg_sum') { + return nestAggregates(records, groupbyLabels, metricLabels, 'sum'); + } + if (timeSeriesOption === 'agg_mean') { + return nestAggregates(records, groupbyLabels, metricLabels, 'mean'); + } + if ( + timeSeriesOption === 'point_diff' || + timeSeriesOption === 'point_factor' || + timeSeriesOption === 'point_percent' + ) { + return nestPointComparison( + records, + groupbyLabels, + metricLabels, + timeSeriesOption, + ); + } + if (timeSeriesOption === 'adv_anal') { + return nestProcs(records, groupbyLabels, options); + } + // 'time_series': aggregate with the timestamp as the first level + return nestAggregates( + records, + [DTTM_ALIAS, ...groupbyLabels], + metricLabels, + 'sum', + ); +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts index 1422701dab26..c22879348599 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts @@ -16,7 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -import { ChartProps } from '@superset-ui/core'; +import { + ChartProps, + ensureIsArray, + getColumnLabel, + getMetricLabel, + QueryFormMetric, +} from '@superset-ui/core'; +import transformData from './transformData'; export default function transformProps(chartProps: ChartProps) { const { width, height, datasource, formData, queriesData } = chartProps; @@ -33,13 +40,34 @@ export default function transformProps(chartProps: ChartProps) { richTooltip, timeSeriesOption, sliceId, + rollingType, + rollingPeriods, + minPeriods, + contribution, } = formData; const { verboseMap = {} } = datasource; + // v1 responses arrive as flat records; the legacy explore_json endpoint + // delivered the nested hierarchy directly. + const rawData = queriesData[0].data; + const data = Array.isArray(rawData) + ? transformData(rawData, { + groupbyLabels: ensureIsArray(groupby).map(getColumnLabel), + metricLabels: ensureIsArray(metrics as QueryFormMetric[]).map( + getMetricLabel, + ), + timeSeriesOption, + rollingType, + rollingPeriods: Number(rollingPeriods) || 0, + minPeriods: Number(minPeriods) || 0, + contribution: Boolean(contribution), + }) + : rawData; + return { width, height, - data: queriesData[0].data, + data, colorScheme, dateTimeFormat, equalDateSize, diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts new file mode 100644 index 000000000000..5eb10557707f --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts @@ -0,0 +1,65 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../src/buildQuery'; + +const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'No filter', + viz_type: 'partition', + groupby: ['gender', 'state'], + metrics: ['sum__num'], +}; + +test('is not a timeseries query for the not-time option', () => { + const [query] = buildQuery({ + ...formData, + time_series_option: 'not_time', + }).queries; + expect(query.is_timeseries).toBe(false); + expect(query.columns).toEqual(['gender', 'state']); + // defaults to ordering by the first metric ascending, like the legacy engine + expect(query.orderby).toEqual([['sum__num', true]]); +}); + +test('returns an empty hierarchy for empty results', () => { + // covered in transformData tests; ordering falls back even when + // order_desc flips the direction + const [query] = buildQuery({ ...formData, order_desc: true }).queries; + expect(query.orderby).toEqual([['sum__num', false]]); +}); + +test('is a timeseries query for time-based options', () => { + const [query] = buildQuery({ + ...formData, + time_series_option: 'point_diff', + }).queries; + expect(query.is_timeseries).toBe(true); +}); + +test('appends the sort metric like the legacy engine', () => { + const [query] = buildQuery({ + ...formData, + timeseries_limit_metric: 'count', + order_desc: true, + }).queries; + expect(query.metrics).toEqual(['sum__num', 'count']); + expect(query.orderby).toEqual([['count', false]]); +}); diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts new file mode 100644 index 000000000000..b2cc541f30f5 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts @@ -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. + */ +import transformData from '../src/transformData'; + +const t1 = 1704067200000; +const t2 = 1704153600000; + +const base = { + groupbyLabels: ['gender', 'state'], + metricLabels: ['sum__num'], +}; + +const records = [ + { __timestamp: t1, gender: 'boy', state: 'CA', sum__num: 10 }, + { __timestamp: t1, gender: 'boy', state: 'NY', sum__num: 20 }, + { __timestamp: t1, gender: 'girl', state: 'CA', sum__num: 30 }, + { __timestamp: t2, gender: 'boy', state: 'CA', sum__num: 40 }, +]; + +test('builds the summed hierarchy for the not-time option', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'not_time', + }); + expect(data).toHaveLength(1); + const root = data[0]; + expect(root.name).toEqual('sum__num'); + expect(root.val).toEqual(100); + expect(root.children.map(child => [child.name, child.val])).toEqual([ + ['boy', 70], + ['girl', 30], + ]); + // deeper levels carry the dim path in the name + expect( + root.children[0].children.map(child => [child.name, child.val]), + ).toEqual([ + [['boy', 'CA'], 50], + [['boy', 'NY'], 20], + ]); +}); + +test('averages instead of sums for agg_mean', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'agg_mean', + }); + expect(data[0].val).toEqual(25); + expect(data[0].children[0].val).toBeCloseTo(70 / 3); +}); + +test('compares the last period against the first for point_diff', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'point_diff', + }); + // until = t2 (40), since = t1 (60) + expect(data[0].val).toEqual(40 - 60); + const boy = data[0].children.find(child => child.name === 'boy')!; + expect(boy.val).toEqual(40 - 30); + // girl has no rows at t2 -> treated as 0 + const girl = data[0].children.find(child => child.name === 'girl')!; + expect(girl.val).toEqual(0 - 30); +}); + +test('uses the timestamp as the first level for time_series', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'time_series', + }); + expect(data[0].children.map(child => child.name)).toEqual([t1, t2]); + expect(data[0].children[0].val).toEqual(60); + expect( + data[0].children[0].children.map(child => [child.name, child.val]), + ).toEqual([ + [[t1, 'boy'], 30], + [[t1, 'girl'], 30], + ]); +}); + +test('nests metric -> time -> groups for period analysis', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'adv_anal', + }); + expect(data[0].name).toEqual('sum__num'); + expect(data[0].val).toBeUndefined(); + expect(data[0].children.map(child => [child.name, child.val])).toEqual([ + [t1, 60], + [t2, 40], + ]); + const firstTime = data[0].children[0]; + expect(firstTime.children.map(child => [child.name, child.val])).toEqual([ + ['boy', 30], + ['girl', 30], + ]); + // missing combinations are pivot-filled with 0 + const secondTime = data[0].children[1]; + expect(secondTime.children.map(child => [child.name, child.val])).toEqual([ + ['boy', 40], + ['girl', 0], + ]); +}); + +test('applies cumulative sums in period analysis', () => { + const data = transformData(records, { + ...base, + timeSeriesOption: 'adv_anal', + rollingType: 'cumsum', + }); + expect(data[0].children.map(child => child.val)).toEqual([60, 100]); +}); + +test('normalizes each period when contribution is on', () => { + const data = transformData(records, { + ...base, + groupbyLabels: ['gender'], + timeSeriesOption: 'adv_anal', + contribution: true, + }); + const firstTime = data[0].children[0]; + expect(firstTime.children.map(child => child.val)).toEqual([0.5, 0.5]); +}); + +test('returns an empty hierarchy for empty results', () => { + expect( + transformData([], { ...base, timeSeriesOption: 'point_diff' }), + ).toEqual([]); +}); + +test('requires at least one groupby', () => { + expect(() => transformData(records, { ...base, groupbyLabels: [] })).toThrow( + 'groupby', + ); +}); From 8948b5af947c0b21f227cc29538ee1e24aa2450f Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 00:16:22 -0700 Subject: [PATCH 18/84] fix(legacy-viz): restore line-engine default ordering for horizon and rose (#41732) Co-authored-by: Claude Code --- .../src/buildQuery.ts | 30 ++++++++++++------- .../test/buildQuery.test.ts | 7 +++++ .../src/buildQuery.ts | 9 ++++++ .../test/buildQuery.test.ts | 7 +++++ 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts index dc195ef0e66f..589f0467d260 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts @@ -16,18 +16,28 @@ * specific language governing permissions and limitations * under the License. */ -import { buildQueryContext, QueryFormData } from '@superset-ui/core'; +import { + buildQueryContext, + ensureIsArray, + QueryFormData, +} from '@superset-ui/core'; /** - * Mirrors the legacy HorizonViz query: a plain timeseries query grouped - * by the dimensions. Series limiting (the `limit` control) and - * order_desc are handled by buildQueryObject. + * Mirrors the legacy HorizonViz query (via NVD3TimeSeriesViz): a + * timeseries query grouped by the dimensions, ordered by the first + * metric ascending unless order_desc. Series limiting (the `limit` + * control) is handled by buildQueryObject. */ export default function buildQuery(formData: QueryFormData) { - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - is_timeseries: true, - }, - ]); + const { order_desc } = formData; + return buildQueryContext(formData, baseQueryObject => { + const firstMetric = ensureIsArray(baseQueryObject.metrics)[0]; + return [ + { + ...baseQueryObject, + is_timeseries: true, + orderby: firstMetric ? [[firstMetric, !order_desc]] : undefined, + }, + ]; + }); } diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts index 52dcc84bade7..de1717a1c218 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts @@ -37,4 +37,11 @@ test('builds a grouped timeseries query with a series limit', () => { expect(query.is_timeseries).toBe(true); expect(query.series_limit).toEqual(25); expect(query.granularity).toEqual('ds'); + // legacy engine default: order by the first metric ascending + expect(query.orderby).toEqual([['sum__num', true]]); +}); + +test('orders descending when order_desc is set', () => { + const [query] = buildQuery({ ...formData, order_desc: true }).queries; + expect(query.orderby).toEqual([['sum__num', false]]); }); diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts index e45103793634..be8b954aecb9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts @@ -57,9 +57,18 @@ export default function buildQuery(rawFormData: QueryFormData) { : rawFormData.comparison_type, }; return buildQueryContext(formData, baseQueryObject => { + const firstMetric = ensureIsArray(baseQueryObject.metrics)[0]; const queryObject = { ...baseQueryObject, is_timeseries: true, + // the legacy engine ordered by the first metric, ascending unless + // order_desc + orderby: firstMetric + ? ([[firstMetric, !formData.order_desc]] as [ + typeof firstMetric, + boolean, + ][]) + : undefined, time_offsets: isTimeComparison(formData, baseQueryObject) ? ensureIsArray(formData.time_compare) : [], diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts index 25e8e3e77e53..7bdf21071ea5 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts @@ -29,6 +29,13 @@ const formData: QueryFormData = { metrics: ['sum__num'], }; +test('orders by the first metric like the legacy engine', () => { + const [query] = buildQuery(formData).queries; + expect(query.orderby).toEqual([['sum__num', true]]); + const [descQuery] = buildQuery({ ...formData, order_desc: true }).queries; + expect(descQuery.orderby).toEqual([['sum__num', false]]); +}); + test('builds a pivot + flatten post-processing pipeline', () => { const [query] = buildQuery(formData).queries; const operations = (query.post_processing || []) From 2cd9873540b23be0a8dd94774bbaf26dfd3db59e Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 00:36:38 -0700 Subject: [PATCH 19/84] feat(bubble): migrate saved bubble charts to bubble_v2 and drop the legacy plugin (#41728) Co-authored-by: Claude Code --- .../src/Bubble/controlPanel.ts | 140 ------- .../src/Bubble/images/example-dark.jpg | Bin 56660 -> 0 bytes .../src/Bubble/images/example.jpg | Bin 61529 -> 0 bytes .../src/Bubble/images/thumbnail-dark.png | Bin 60642 -> 0 bytes .../src/Bubble/images/thumbnail.png | Bin 20423 -> 0 bytes .../src/Bubble/images/thumbnailLarge.png | Bin 45424 -> 0 bytes .../src/Bubble/index.ts | 63 ---- .../src/Bubble/stories/NVD3Bubble.stories.tsx | 103 ----- .../src/Bubble/stories/data.ts | 357 ------------------ .../legacy-preset-chart-nvd3/src/NVD3Vis.ts | 24 -- .../legacy-preset-chart-nvd3/src/index.ts | 1 - .../legacy-preset-chart-nvd3/src/preset.ts | 2 - .../src/visualizations/presets/MainPreset.ts | 2 - superset/examples/world_bank.py | 6 +- ...6a7b8c9_migrate_bubble_chart_to_echarts.py | 55 +++ superset/viz.py | 54 +-- tests/integration_tests/core_tests.py | 6 +- tests/integration_tests/utils_tests.py | 2 +- 18 files changed, 63 insertions(+), 752 deletions(-) delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/controlPanel.ts delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example-dark.jpg delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example.jpg delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail-dark.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnailLarge.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.ts delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/NVD3Bubble.stories.tsx delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/data.ts create mode 100644 superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/controlPanel.ts deleted file mode 100644 index f6073a370955..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/controlPanel.ts +++ /dev/null @@ -1,140 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - formatSelectOptions, - D3_FORMAT_OPTIONS, - getStandardizedControls, -} from '@superset-ui/chart-controls'; -import { - showLegend, - xAxisLabel, - yAxisLabel, - bottomMargin, - xTicksLayout, - xAxisFormat, - yLogScale, - xAxisShowMinmax, - yAxisShowMinmax, - leftMargin, - yAxisBounds, -} from '../NVD3Controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['series'], - ['entity'], - ['x'], - ['y'], - ['adhoc_filters'], - ['size'], - [ - { - name: 'max_bubble_size', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Max Bubble Size'), - default: '25', - choices: formatSelectOptions([ - '5', - '10', - '15', - '25', - '50', - '75', - '100', - ]), - }, - }, - ], - ['limit', null], - ], - }, - { - label: t('Chart Options'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [['color_scheme'], [showLegend, null]], - }, - { - label: t('X Axis'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - [xAxisLabel, leftMargin], - [ - { - name: xAxisFormat.name, - config: { - ...xAxisFormat.config, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - }, - }, - xTicksLayout, - ], - [ - { - name: 'x_log_scale', - config: { - type: 'CheckboxControl', - label: t('X Log Scale'), - default: false, - renderTrigger: true, - description: t('Use a log scale for the X-axis'), - }, - }, - xAxisShowMinmax, - ], - ], - }, - { - label: t('Y Axis'), - expanded: true, - tabOverride: 'customize', - controlSetRows: [ - [yAxisLabel, bottomMargin], - ['y_axis_format', null], - [yLogScale, yAxisShowMinmax], - [yAxisBounds], - ], - }, - ], - controlOverrides: { - color_scheme: { - renderTrigger: false, - }, - }, - formDataOverrides: formData => ({ - ...formData, - series: getStandardizedControls().shiftColumn(), - entity: getStandardizedControls().shiftColumn(), - x: getStandardizedControls().shiftMetric(), - y: getStandardizedControls().shiftMetric(), - size: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example-dark.jpg b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example-dark.jpg deleted file mode 100644 index 1e35fc3455d3dced649cd08ca37e99230dd1a937..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56660 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 z 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 ZGz#SBes5;@*ZDt2cKKh{PWa!){~v3;D1ZO} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example.jpg b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/example.jpg deleted file mode 100644 index b515822fa2c945135da16f4fd7de1462ccbd43a1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61529 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�Fi-UBU>h&liinIa(C#p-C&YO~@c>&T5M&0eFWV z%{b6)(^bKD8WVZjJL63GEtpsd3rWops8dFhZ#!mrwRw@9{dV~W^Mg*y0V=|?Nj)il zI^8FdMX)g47O)oWowJknrXP7;#5BA6Xeh!l>r8+~<0i(toufinhdHd)m))oj=5YUUgjtk@08x{CHzZ&U6PqFB5dm-Qm| zwK}y4Q&$}Tr4~2fJa1db$~9K1;L8Tf(|pxx$7?~I0iI+V!W01rc$ZPRC0OLXnY!kBerVQJ3RDW5QGP*7TRN3ZNoi&z%VC${qi-78Yx6Pi#? zi)0cXqzCl2dE%s^{P|1a*vm6oJ9Z8c8CxWvXYoTUh>NHJh0bcKIRhD;$AXQ;QZ|@+ z`&8=e#VpY$BfdkGOTIHPuqp!m8BBoYvuohI9_dTrev$t;k#nu$<`e=HYfI7=T*qSb ze9rdd?nq9W7LiEY+V@f|4Y`yF;%9P(fi~;>BhWPY|7g$45k@FR(uFc_m8HncUToHYs-SSdBuBY)82*@7amKspE9*GAuCOnS2`|< zSsIUpGN1-NUU=!qu2~?ctH?H#H~n%+lkO+BQZKW6si{Qj$(}6Fc8_} zYjgn%z`IpbTncq#rL8q)?KWG29m_O9L~4RdAL`Ii$t780m?qY4e>{?*@Ei@F=P$0$3#D9%p3 zjPf5&?{Y~*Nb6U6t7{kjyUD<8cXH$B^Sli&)K!ltR(opB^I?cg6Zh2 z90SM2tOUy%n0rPR1i0~@z?4RXjY!8@D1XbQH1yja{q>K+Uv4OV|CZmb13%~

*uK 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 ZVSgUYTg2rk1^9p6iTq!22il(p{}+DG5)1$U diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail-dark.png b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail-dark.png deleted file mode 100644 index da9aa70bc1272ea9d35143ad5a4a0686d04ac644..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60642 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 HdGP-Lk2WQf diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail.png b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnail.png deleted file mode 100644 index c27ba4719719fdf7d79165b383303fc6caf88d4a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20423 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 Xsi{c1-#i8(|4r4^G|(thvkv?ZQO_0R diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnailLarge.png b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/images/thumbnailLarge.png deleted file mode 100644 index c1127688efc187fdda54a66b653c3719b83d9475..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45424 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 import('../ReactNVD3'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/NVD3Bubble.stories.tsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/NVD3Bubble.stories.tsx deleted file mode 100644 index ff97de12b3d1..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/NVD3Bubble.stories.tsx +++ /dev/null @@ -1,103 +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 { BubbleChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3'; -import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; -import data from './data'; - -new BubbleChartPlugin().configure({ key: VizType.LegacyBubble }).register(); - -export default { - title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Bubble', - decorators: [withResizableChartDemo], - args: { - colorScheme: 'd3Category10', - maxBubbleSize: 50, - showLegend: true, - xLogScale: false, - yLogScale: false, - }, - argTypes: { - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - maxBubbleSize: { - control: { type: 'range', min: 10, max: 100, step: 5 }, - }, - showLegend: { control: 'boolean' }, - xLogScale: { control: 'boolean' }, - yLogScale: { control: 'boolean' }, - }, -}; - -export const Basic = ({ - colorScheme, - maxBubbleSize, - showLegend, - xLogScale, - yLogScale, - width, - height, -}: { - colorScheme: string; - maxBubbleSize: number; - showLegend: boolean; - xLogScale: boolean; - yLogScale: boolean; - width: number; - height: number; -}) => ( - -); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/data.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/data.ts deleted file mode 100644 index 167f36303952..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/stories/data.ts +++ /dev/null @@ -1,357 +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 */ -export default [ - { - key: 'East Asia & Pacific', - values: [ - { - country_name: 'China', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 1344130000.0, - sum__SP_RUR_TOTL_ZS: 49.427, - sum__SP_DYN_LE00_IN: 75.042, - x: 49.427, - y: 75.042, - size: 1344130000.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Indonesia', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 244808254.0, - sum__SP_RUR_TOTL_ZS: 49.288, - sum__SP_DYN_LE00_IN: 70.3915609756, - x: 49.288, - y: 70.3915609756, - size: 244808254.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Japan', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 127817277.0, - sum__SP_RUR_TOTL_ZS: 8.752, - sum__SP_DYN_LE00_IN: 82.5912195122, - x: 8.752, - y: 82.5912195122, - size: 127817277.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Philippines', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 94501233.0, - sum__SP_RUR_TOTL_ZS: 54.983, - sum__SP_DYN_LE00_IN: 68.3914878049, - x: 54.983, - y: 68.3914878049, - size: 94501233.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Vietnam', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 87840000.0, - sum__SP_RUR_TOTL_ZS: 68.971, - sum__SP_DYN_LE00_IN: 75.457902439, - x: 68.971, - y: 75.457902439, - size: 87840000.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Thailand', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 66902958.0, - sum__SP_RUR_TOTL_ZS: 54.606, - sum__SP_DYN_LE00_IN: 74.008902439, - x: 54.606, - y: 74.008902439, - size: 66902958.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - { - country_name: 'Myanmar', - region: 'East Asia & Pacific', - sum__SP_POP_TOTL: 52125411.0, - sum__SP_RUR_TOTL_ZS: 68.065, - sum__SP_DYN_LE00_IN: 64.7612439024, - x: 68.065, - y: 64.7612439024, - size: 52125411.0, - shape: 'circle', - group: 'East Asia & Pacific', - }, - ], - }, - { - key: 'South Asia', - values: [ - { - country_name: 'India', - region: 'South Asia', - sum__SP_POP_TOTL: 1247446011.0, - sum__SP_RUR_TOTL_ZS: 68.724, - sum__SP_DYN_LE00_IN: 65.9584878049, - x: 68.724, - y: 65.9584878049, - size: 1247446011.0, - shape: 'circle', - group: 'South Asia', - }, - { - country_name: 'Pakistan', - region: 'South Asia', - sum__SP_POP_TOTL: 173669648.0, - sum__SP_RUR_TOTL_ZS: 62.993, - sum__SP_DYN_LE00_IN: 66.2838780488, - x: 62.993, - y: 66.2838780488, - size: 173669648.0, - shape: 'circle', - group: 'South Asia', - }, - { - country_name: 'Bangladesh', - region: 'South Asia', - sum__SP_POP_TOTL: 153405612.0, - sum__SP_RUR_TOTL_ZS: 68.775, - sum__SP_DYN_LE00_IN: 69.891804878, - x: 68.775, - y: 69.891804878, - size: 153405612.0, - shape: 'circle', - group: 'South Asia', - }, - ], - }, - { - key: 'North America', - values: [ - { - country_name: 'United States', - region: 'North America', - sum__SP_POP_TOTL: 311721632.0, - sum__SP_RUR_TOTL_ZS: 19.06, - sum__SP_DYN_LE00_IN: 78.6414634146, - x: 19.06, - y: 78.6414634146, - size: 311721632.0, - shape: 'circle', - group: 'North America', - }, - ], - }, - { - key: 'Latin America & Caribbean', - values: [ - { - country_name: 'Brazil', - region: 'Latin America & Caribbean', - sum__SP_POP_TOTL: 200517584.0, - sum__SP_RUR_TOTL_ZS: 15.377, - sum__SP_DYN_LE00_IN: 73.3473658537, - x: 15.377, - y: 73.3473658537, - size: 200517584.0, - shape: 'circle', - group: 'Latin America & Caribbean', - }, - { - country_name: 'Mexico', - region: 'Latin America & Caribbean', - sum__SP_POP_TOTL: 120365271.0, - sum__SP_RUR_TOTL_ZS: 21.882, - sum__SP_DYN_LE00_IN: 76.9141707317, - x: 21.882, - y: 76.9141707317, - size: 120365271.0, - shape: 'circle', - group: 'Latin America & Caribbean', - }, - ], - }, - { - key: 'Sub-Saharan Africa', - values: [ - { - country_name: 'Nigeria', - region: 'Sub-Saharan Africa', - sum__SP_POP_TOTL: 163770669.0, - sum__SP_RUR_TOTL_ZS: 55.638, - sum__SP_DYN_LE00_IN: 51.7102439024, - x: 55.638, - y: 51.7102439024, - size: 163770669.0, - shape: 'circle', - group: 'Sub-Saharan Africa', - }, - { - country_name: 'Ethiopia', - region: 'Sub-Saharan Africa', - sum__SP_POP_TOTL: 89858696.0, - sum__SP_RUR_TOTL_ZS: 82.265, - sum__SP_DYN_LE00_IN: 62.2528536585, - x: 82.265, - y: 62.2528536585, - size: 89858696.0, - shape: 'circle', - group: 'Sub-Saharan Africa', - }, - { - country_name: 'Congo, Dem. Rep.', - region: 'Sub-Saharan Africa', - sum__SP_POP_TOTL: 68087376.0, - sum__SP_RUR_TOTL_ZS: 59.558, - sum__SP_DYN_LE00_IN: 49.3007073171, - x: 59.558, - y: 49.3007073171, - size: 68087376.0, - shape: 'circle', - group: 'Sub-Saharan Africa', - }, - { - country_name: 'South Africa', - region: 'Sub-Saharan Africa', - sum__SP_POP_TOTL: 51553479.0, - sum__SP_RUR_TOTL_ZS: 37.254, - sum__SP_DYN_LE00_IN: 55.2956585366, - x: 37.254, - y: 55.2956585366, - size: 51553479.0, - shape: 'circle', - group: 'Sub-Saharan Africa', - }, - ], - }, - { - key: 'Europe & Central Asia', - values: [ - { - country_name: 'Russian Federation', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 142960868.0, - sum__SP_RUR_TOTL_ZS: 26.268, - sum__SP_DYN_LE00_IN: 69.6585365854, - x: 26.268, - y: 69.6585365854, - size: 142960868.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - { - country_name: 'Germany', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 81797673.0, - sum__SP_RUR_TOTL_ZS: 25.512, - sum__SP_DYN_LE00_IN: 80.7414634146, - x: 25.512, - y: 80.7414634146, - size: 81797673.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - { - country_name: 'Turkey', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 73199372.0, - sum__SP_RUR_TOTL_ZS: 28.718, - sum__SP_DYN_LE00_IN: 74.5404878049, - x: 28.718, - y: 74.5404878049, - size: 73199372.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - { - country_name: 'France', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 65342776.0, - sum__SP_RUR_TOTL_ZS: 21.416, - sum__SP_DYN_LE00_IN: 82.1146341463, - x: 21.416, - y: 82.1146341463, - size: 65342776.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - { - country_name: 'United Kingdom', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 63258918.0, - sum__SP_RUR_TOTL_ZS: 18.43, - sum__SP_DYN_LE00_IN: 80.9512195122, - x: 18.43, - y: 80.9512195122, - size: 63258918.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - { - country_name: 'Italy', - region: 'Europe & Central Asia', - sum__SP_POP_TOTL: 59379449.0, - sum__SP_RUR_TOTL_ZS: 31.556, - sum__SP_DYN_LE00_IN: 82.187804878, - x: 31.556, - y: 82.187804878, - size: 59379449.0, - shape: 'circle', - group: 'Europe & Central Asia', - }, - ], - }, - { - key: 'Middle East & North Africa', - values: [ - { - country_name: 'Egypt, Arab Rep.', - region: 'Middle East & North Africa', - sum__SP_POP_TOTL: 83787634.0, - sum__SP_RUR_TOTL_ZS: 57.0, - sum__SP_DYN_LE00_IN: 70.6785609756, - x: 57.0, - y: 70.6785609756, - size: 83787634.0, - shape: 'circle', - group: 'Middle East & North Africa', - }, - { - country_name: 'Iran, Islamic Rep.', - region: 'Middle East & North Africa', - sum__SP_POP_TOTL: 75184322.0, - sum__SP_RUR_TOTL_ZS: 28.8, - sum__SP_DYN_LE00_IN: 73.4493170732, - x: 28.8, - y: 73.4493170732, - size: 75184322.0, - shape: 'circle', - group: 'Middle East & North Africa', - }, - ], - }, -]; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts index a34e61e68f3f..0531a8d869b3 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts @@ -47,7 +47,6 @@ import { cleanColorInput, computeYDomain, drawBarValues, - generateBubbleTooltipContent, generateCompareTooltipContent, generateTimePivotTooltip, generateTooltipClassName, @@ -407,29 +406,6 @@ function nvd3Vis(element, props) { chart.xAxis.showMaxMin(false); break; - case VizType.LegacyBubble: - chart = nv.models.scatterChart(); - chart.showDistX(false); - chart.showDistY(false); - chart.tooltip.contentGenerator(d => - generateBubbleTooltipContent({ - point: d.point, - entity, - xField, - yField, - sizeField, - xFormatter: getTimeOrNumberFormatter(xAxisFormat), - yFormatter: getTimeOrNumberFormatter(yAxisFormat), - sizeFormatter: formatter, - }), - ); - chart.pointRange([5, maxBubbleSize ** 2]); - chart.pointDomain([ - 0, - d3.max(data, d => d3.max(d.values, v => v.size)), - ]); - break; - case VizType.BoxPlot: colorKey = 'label'; chart = nv.models.boxPlotChart(); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts index 2b26d4d67db9..eb77d57c6a36 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts @@ -18,7 +18,6 @@ */ export { default as NVD3ChartPreset } from './preset'; -export { default as BubbleChartPlugin } from './Bubble'; export { default as BulletChartPlugin } from './Bullet'; export { default as CompareChartPlugin } from './Compare'; export { default as TimePivotChartPlugin } from './TimePivot'; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts index 09a636b39342..584579488fcd 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts @@ -17,7 +17,6 @@ * under the License. */ import { Preset, VizType } from '@superset-ui/core'; -import BubbleChartPlugin from './Bubble'; import BulletChartPlugin from './Bullet'; import CompareChartPlugin from './Compare'; import TimePivotChartPlugin from './TimePivot'; @@ -27,7 +26,6 @@ export default class NVD3ChartPreset extends Preset { super({ name: 'NVD3 charts', plugins: [ - new BubbleChartPlugin().configure({ key: VizType.LegacyBubble }), new BulletChartPlugin().configure({ key: VizType.Bullet }), new CompareChartPlugin().configure({ key: VizType.Compare }), new TimePivotChartPlugin().configure({ key: VizType.TimePivot }), diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index d1a12de0fb40..fc76bb907a09 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -34,7 +34,6 @@ import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; import WorldMapChartPlugin from '@superset-ui/legacy-plugin-chart-world-map'; import { - BubbleChartPlugin, BulletChartPlugin, CompareChartPlugin, TimePivotChartPlugin, @@ -114,7 +113,6 @@ export default class MainPreset extends Preset { key: VizType.BigNumberTotal, }), new EchartsBoxPlotChartPlugin().configure({ key: VizType.BoxPlot }), - new BubbleChartPlugin().configure({ key: VizType.LegacyBubble }), new BulletChartPlugin().configure({ key: VizType.Bullet }), new CalendarChartPlugin().configure({ key: VizType.Calendar }), new ChordChartPlugin().configure({ key: VizType.Chord }), diff --git a/superset/examples/world_bank.py b/superset/examples/world_bank.py index c81c0b7b5ee6..7b52226d0ecd 100644 --- a/superset/examples/world_bank.py +++ b/superset/examples/world_bank.py @@ -222,16 +222,16 @@ def create_slices(tbl: BaseDatasource) -> list[Slice]: ), Slice( slice_name="Life Expectancy VS Rural %", - viz_type="bubble", + viz_type="bubble_v2", datasource_type=DatasourceType.TABLE, datasource_id=tbl.id, params=get_slice_json( defaults, - viz_type="bubble", + viz_type="bubble_v2", since="2011-01-01", until="2011-01-02", series="region", - limit=0, + row_limit=0, entity="country_name", x="sum__SP_RUR_TOTL_ZS", y="sum__SP_DYN_LE00_IN", diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py new file mode 100644 index 000000000000..d80c8b50dd92 --- /dev/null +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -0,0 +1,55 @@ +# 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. +"""migrate_bubble_chart_to_echarts + +Revision ID: d4e5f6a7b8c9 +Revises: b4a3f2e1d0c9 +Create Date: 2026-07-02 21:00:00.000000 + +""" + +from alembic import op +from sqlalchemy import text +from sqlalchemy.dialects.mysql.base import MySQLDialect + +from superset import db +from superset.migrations.shared.migrate_viz import MigrateBubbleChart + +# revision identifiers, used by Alembic. +revision = "d4e5f6a7b8c9" +down_revision = "b4a3f2e1d0c9" + + +def upgrade(): + bind = op.get_bind() + + # Ensure `slice.params` and `slice.query_context` in MySQL is MEDIUMTEXT + # before migration, as the migration will save a duplicate form_data backup + # which may significantly increase the size of these fields. + if isinstance(bind.dialect, MySQLDialect): + # If the columns are already MEDIUMTEXT, this is a no-op + op.execute(text("ALTER TABLE slices MODIFY params MEDIUMTEXT")) + op.execute(text("ALTER TABLE slices MODIFY query_context MEDIUMTEXT")) + + session = db.Session(bind=bind) + MigrateBubbleChart.upgrade(session) + + +def downgrade(): + bind = op.get_bind() + session = db.Session(bind=bind) + MigrateBubbleChart.downgrade(session) diff --git a/superset/viz.py b/superset/viz.py index 699706785d4b..56e6001f2b7e 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -27,7 +27,7 @@ import dataclasses import logging import math -from collections import defaultdict, OrderedDict +from collections import OrderedDict from datetime import datetime, timedelta from itertools import product from typing import Any, cast, Optional, TYPE_CHECKING @@ -884,58 +884,6 @@ class NVD3Viz(BaseViz): is_timeseries = False -class BubbleViz(NVD3Viz): - """Based on the NVD3 bubble chart""" - - viz_type = "bubble" - verbose_name = _("Bubble Chart") - is_timeseries = False - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["groupby"] = [self.form_data.get("entity")] - if self.form_data.get("series"): - query_obj["groupby"].append(self.form_data.get("series")) - - # dedup groupby if it happens to be the same - query_obj["groupby"] = self.dedup_columns(query_obj["groupby"]) - - # pylint: disable=attribute-defined-outside-init - self.x_metric = self.form_data["x"] - self.y_metric = self.form_data["y"] - self.z_metric = self.form_data["size"] - self.entity = self.form_data.get("entity") - self.series = self.form_data.get("series") or self.entity - query_obj["row_limit"] = self.form_data.get("limit") - - query_obj["metrics"] = [self.z_metric, self.x_metric, self.y_metric] - if len(set(self.metric_labels)) < 3: - raise QueryObjectValidationError(_("Please use 3 different metric labels")) - if not all(query_obj["metrics"] + [self.entity]): - raise QueryObjectValidationError(_("Pick a metric for x, y and size")) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - df["x"] = df[[utils.get_metric_name(self.x_metric)]] - df["y"] = df[[utils.get_metric_name(self.y_metric)]] - df["size"] = df[[utils.get_metric_name(self.z_metric)]] - df["shape"] = "circle" - df["group"] = df[[get_column_name(self.series)]] # type: ignore - - series: dict[Any, list[Any]] = defaultdict(list) - for row in df.to_dict(orient="records"): - series[row["group"]].append(row) - chart_data = [] - for k, v in series.items(): - chart_data.append({"key": k, "values": v}) - return chart_data - - class BulletViz(NVD3Viz): """Based on the NVD3 bullet chart""" diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 459ac27f443e..0a4ed98b2b2f 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -123,7 +123,7 @@ def test_slice_endpoint(self): @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") def test_viz_cache_key(self): self.login(ADMIN_USERNAME) - slc = self.get_slice("Life Expectancy VS Rural %") + slc = self.get_slice("% Rural") viz = slc.viz qobj = viz.query_obj() @@ -486,7 +486,7 @@ def test_explore_json_propagates_oauth2_redirect_error( ) self.login(ADMIN_USERNAME) - slc = self.get_slice("Life Expectancy VS Rural %") + slc = self.get_slice("% Rural") rv = self.client.post( f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/", data={"form_data": json.dumps(slc.form_data)}, @@ -513,7 +513,7 @@ def test_explore_json_generic_exception_still_returns_viz_get_df_error( mock_get_df.side_effect = RuntimeError("boom") self.login(ADMIN_USERNAME) - slc = self.get_slice("Life Expectancy VS Rural %") + slc = self.get_slice("% Rural") rv = self.client.post( f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/", data={"form_data": json.dumps(slc.form_data)}, diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py index c1b98ca3c355..fc083cd7e3ca 100644 --- a/tests/integration_tests/utils_tests.py +++ b/tests/integration_tests/utils_tests.py @@ -358,7 +358,7 @@ def test_get_form_data_corrupted_json(self) -> None: def test_log_this(self) -> None: # TODO: Add additional scenarios. self.login(ADMIN_USERNAME) - slc = self.get_slice("Life Expectancy VS Rural %") + slc = self.get_slice("% Rural") dashboard_id = 1 assert slc.viz is not None From b2849d826afff411856d01d8245632bcaa366a43 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 00:38:52 -0700 Subject: [PATCH 20/84] feat(deck-multi): migrate deck_multi chart to the v1 chart data API (#41730) Co-authored-by: Claude Code --- .../src/Multi/Multi.test.tsx | 138 ++++++++--------- .../preset-chart-deckgl/src/Multi/Multi.tsx | 145 ++++++++++++++---- .../src/Multi/buildQuery.ts | 28 ++++ .../preset-chart-deckgl/src/Multi/index.ts | 2 +- .../charts/data/test_empty_query_context.py | 43 ++++++ 5 files changed, 249 insertions(+), 107 deletions(-) create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.ts create mode 100644 tests/unit_tests/charts/data/test_empty_query_context.py diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.test.tsx index 082c789c6a8f..a916f782aacc 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.test.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.test.tsx @@ -43,9 +43,29 @@ jest.mock('@superset-ui/core', () => ({ ...jest.requireActual('@superset-ui/core'), SupersetClient: { get: jest.fn(), + post: jest.fn(), }, })); +// register stub buildQuery/transformProps for the layer types the tests use +const { getChartBuildQueryRegistry, getChartTransformPropsRegistry } = + jest.requireActual('@superset-ui/core'); +['deck_scatter', 'deck_polygon'].forEach(vizType => { + getChartBuildQueryRegistry().registerValue( + vizType, + (formData: Record) => ({ + datasource: 'test_datasource', + queries: [{}], + form_data: formData, + }), + ); + getChartTransformPropsRegistry().registerValue(vizType, () => ({ + payload: { + data: { features: [], mapboxApiKey: 'test-key', metricLabels: [] }, + }, + })); +}); + const mockStore = configureStore({ reducer: { dataMask: () => ({}), @@ -128,6 +148,11 @@ const renderWithProviders = (component: React.ReactElement) => describe('DeckMulti Autozoom Functionality', () => { beforeEach(() => { jest.clearAllMocks(); + (SupersetClient.post as jest.Mock).mockResolvedValue({ + json: { + result: [{ data: [] }], + }, + }); (SupersetClient.get as jest.Mock).mockResolvedValue({ json: { data: { @@ -451,6 +476,11 @@ describe('DeckMulti Autozoom Functionality', () => { describe('DeckMulti Component Rendering', () => { beforeEach(() => { jest.clearAllMocks(); + (SupersetClient.post as jest.Mock).mockResolvedValue({ + json: { + result: [{ data: [] }], + }, + }); (SupersetClient.get as jest.Mock).mockResolvedValue({ json: { data: { @@ -498,16 +528,13 @@ describe('DeckMulti Component Rendering', () => { // Wait for child slice requests await waitFor(() => { - expect(SupersetClient.get).toHaveBeenCalled(); + expect(SupersetClient.post).toHaveBeenCalled(); }); // Check that all requests include the dashboardId - const { calls } = (SupersetClient.get as jest.Mock).mock; + const { calls } = (SupersetClient.post as jest.Mock).mock; calls.forEach(call => { - const url = call[0].endpoint; - const urlParams = new URLSearchParams(url.split('?')[1]); - const formDataString = urlParams.get('form_data'); - const formData = JSON.parse(formDataString || '{}'); + const formData = call[0].jsonPayload?.form_data || {}; expect(formData.dashboardId).toBe(123); }); }); @@ -525,16 +552,13 @@ describe('DeckMulti Component Rendering', () => { // Wait for child slice requests await waitFor(() => { - expect(SupersetClient.get).toHaveBeenCalled(); + expect(SupersetClient.post).toHaveBeenCalled(); }); // Check that requests don't include dashboardId - const { calls } = (SupersetClient.get as jest.Mock).mock; + const { calls } = (SupersetClient.post as jest.Mock).mock; calls.forEach(call => { - const url = call[0].endpoint; - const formData = JSON.parse( - new URLSearchParams(url.split('?')[1]).get('form_data') || '{}', - ); + const formData = call[0].jsonPayload?.form_data || {}; expect(formData.dashboardId).toBeUndefined(); }); }); @@ -553,16 +577,13 @@ describe('DeckMulti Component Rendering', () => { // Wait for child slice requests await waitFor(() => { - expect(SupersetClient.get).toHaveBeenCalled(); + expect(SupersetClient.post).toHaveBeenCalled(); }); // Verify dashboardId is preserved with filters - const { calls } = (SupersetClient.get as jest.Mock).mock; + const { calls } = (SupersetClient.post as jest.Mock).mock; calls.forEach(call => { - const url = call[0].endpoint; - const formData = JSON.parse( - new URLSearchParams(url.split('?')[1]).get('form_data') || '{}', - ); + const formData = call[0].jsonPayload?.form_data || {}; expect(formData.dashboardId).toBe(456); expect(formData.extra_filters).toBeDefined(); }); @@ -608,20 +629,12 @@ describe('DeckMulti Component Rendering', () => { test('includes parent_slice_id in child slice requests when parent has slice_id', async () => { jest.clearAllMocks(); - const mockGet = jest.fn().mockResolvedValue({ + const mockPost = jest.fn().mockResolvedValue({ json: { - result: { - form_data: { - viz_type: 'deck_scatter', - datasource: '1__table', - }, - }, - data: { - features: [], - }, + result: [{ data: [] }], }, }); - (SupersetClient.get as jest.Mock) = mockGet; + (SupersetClient.post as jest.Mock) = mockPost; const parentSliceId = 99; const dashboardId = 5; @@ -637,39 +650,28 @@ test('includes parent_slice_id in child slice requests when parent has slice_id' renderWithProviders(); await waitFor(() => { - expect(mockGet).toHaveBeenCalled(); + expect(mockPost).toHaveBeenCalled(); }); // Check that the child slice requests include parent_slice_id - const { calls } = mockGet.mock; + const { calls } = mockPost.mock; calls.forEach(call => { - const { endpoint } = call[0]; - if (endpoint.includes('api/v1/explore/form_data')) { - const body = JSON.parse(call[0].body); - expect(body.form_data).toMatchObject({ - dashboardId, - parent_slice_id: parentSliceId, - }); - } + const formData = call[0].jsonPayload?.form_data || {}; + expect(formData).toMatchObject({ + dashboardId, + parent_slice_id: parentSliceId, + }); }); }); test('includes parent_slice_id in embedded mode', async () => { jest.clearAllMocks(); - const mockGet = jest.fn().mockResolvedValue({ + const mockPost = jest.fn().mockResolvedValue({ json: { - result: { - form_data: { - viz_type: 'deck_scatter', - datasource: '1__table', - }, - }, - data: { - features: [], - }, + result: [{ data: [] }], }, }); - (SupersetClient.get as jest.Mock) = mockGet; + (SupersetClient.post as jest.Mock) = mockPost; const parentSliceId = 200; const dashboardId = 10; @@ -686,36 +688,25 @@ test('includes parent_slice_id in embedded mode', async () => { renderWithProviders(); await waitFor(() => { - expect(mockGet).toHaveBeenCalled(); + expect(mockPost).toHaveBeenCalled(); }); // Verify parent_slice_id is included in embedded mode - const { calls } = mockGet.mock; + const { calls } = mockPost.mock; calls.forEach(call => { - const { endpoint } = call[0]; - if (endpoint.includes('api/v1/explore/form_data')) { - const body = JSON.parse(call[0].body); - expect(body.form_data.parent_slice_id).toBe(parentSliceId); - } + const formData = call[0].jsonPayload?.form_data || {}; + expect(formData.parent_slice_id).toBe(parentSliceId); }); }); test('does not include parent_slice_id when parent has no slice_id', async () => { jest.clearAllMocks(); - const mockGet = jest.fn().mockResolvedValue({ + const mockPost = jest.fn().mockResolvedValue({ json: { - result: { - form_data: { - viz_type: 'deck_scatter', - datasource: '1__table', - }, - }, - data: { - features: [], - }, + result: [{ data: [] }], }, }); - (SupersetClient.get as jest.Mock) = mockGet; + (SupersetClient.post as jest.Mock) = mockPost; const props = { ...baseMockProps, @@ -729,16 +720,13 @@ test('does not include parent_slice_id when parent has no slice_id', async () => renderWithProviders(); await waitFor(() => { - expect(mockGet).toHaveBeenCalled(); + expect(mockPost).toHaveBeenCalled(); }); // Verify parent_slice_id is not included when parent has no slice_id - const { calls } = mockGet.mock; + const { calls } = mockPost.mock; calls.forEach(call => { - const { endpoint } = call[0]; - if (endpoint.includes('api/v1/explore/form_data')) { - const body = JSON.parse(call[0].body); - expect(body.form_data.parent_slice_id).toBeUndefined(); - } + const formData = call[0].jsonPayload?.form_data || {}; + expect(formData.parent_slice_id).toBeUndefined(); }); }); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index 7fa89ba1ef07..50387dd0744b 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -25,12 +25,15 @@ import { isEqual } from 'lodash-es'; import { createSelector } from '@reduxjs/toolkit'; import { AdhocFilter, + ChartProps, ContextMenuFilters, DataMask, Datasource, ensureIsArray, ExtraFormData, FilterState, + getChartBuildQueryRegistry, + getChartTransformPropsRegistry, HandlerFunction, isDefined, JsonObject, @@ -41,14 +44,13 @@ import { getMapProviderMapStyle, usePrevious, } from '@superset-ui/core'; -import { styled } from '@apache-superset/core/theme'; +import { styled, useTheme } from '@apache-superset/core/theme'; import { Layer } from '@deck.gl/core'; import { DeckGLContainerHandle, DeckGLContainerStyledWrapper, } from '../DeckGLContainer'; -import { getExploreLongUrl } from '../utils/explore'; import { addColorToFeatures } from '../utils/addColor'; import { COLOR_SCHEME_TYPES, ColorSchemeType } from '../utilities/utils'; import layerGenerators from '../layers'; @@ -117,6 +119,7 @@ const selectDataMask = createSelector( const DeckMulti = (props: DeckMultiProps) => { const containerRef = useRef(); + const theme = useTheme(); const dataMask = useSelector(selectDataMask); @@ -131,20 +134,21 @@ const DeckMulti = (props: DeckMultiProps) => { let viewport = { ...props.viewport }; // Default to autozoom enabled for backward compatibility (undefined treated as true) + // legacy container payloads carried pre-merged features; the v1 path + // fetches every layer client-side, so there may be none here + const features = props.payload?.data?.features || {}; if (props.formData.autozoom !== false) { const points = [ - ...getPointsPolygon(props.payload.data.features.deck_polygon || []), - ...getPointsPath(props.payload.data.features.deck_path || []), - ...getPointsGrid(props.payload.data.features.deck_grid || []), - ...getPointsScatter(props.payload.data.features.deck_scatter || []), - ...getPointsContour(props.payload.data.features.deck_contour || []), - ...getPointsHeatmap(props.payload.data.features.deck_heatmap || []), - ...getPointsHex(props.payload.data.features.deck_hex || []), - ...getPointsArc(props.payload.data.features.deck_arc || []), - ...getPointsGeojson(props.payload.data.features.deck_geojson || []), - ...getPointsScreengrid( - props.payload.data.features.deck_screengrid || [], - ), + ...getPointsPolygon(features.deck_polygon || []), + ...getPointsPath(features.deck_path || []), + ...getPointsGrid(features.deck_grid || []), + ...getPointsScatter(features.deck_scatter || []), + ...getPointsContour(features.deck_contour || []), + ...getPointsHeatmap(features.deck_heatmap || []), + ...getPointsHex(features.deck_hex || []), + ...getPointsArc(features.deck_arc || []), + ...getPointsGeojson(features.deck_geojson || []), + ...getPointsScreengrid(features.deck_screengrid || []), ]; if (props.formData && points.length > 0) { @@ -336,31 +340,68 @@ const DeckMulti = (props: DeckMultiProps) => { }, } as any as JsonObject & { slice_id: number }; - const url = getExploreLongUrl(subsliceCopy.form_data, 'json'); - - if (url) { - SupersetClient.get({ endpoint: url }) - .then(({ json }) => { - const layer = createLayerFromData(subsliceCopy, json); + const vizType = subsliceCopy.form_data.viz_type as string; + Promise.all([ + getChartBuildQueryRegistry().get(vizType), + getChartTransformPropsRegistry().get(vizType), + ]) + .then(([layerBuildQuery, layerTransformProps]) => { + if ( + typeof layerBuildQuery !== 'function' || + typeof layerTransformProps !== 'function' + ) { + throw new Error(`Unknown deck.gl layer type: ${vizType}`); + } + const queryContext = layerBuildQuery( + subsliceCopy.form_data as QueryFormData, + ); + return SupersetClient.post({ + endpoint: '/api/v1/chart/data', + jsonPayload: { + ...queryContext, + result_format: 'json', + result_type: 'full', + }, + }).then(({ json }) => { + const chartProps = new ChartProps({ + width: props.width, + height: props.height, + datasource: props.datasource as unknown as JsonObject, + formData: subsliceCopy.form_data, + queriesData: (json as JsonObject).result, + theme, + hooks: {}, + initialValues: {}, + }); + const layerProps = layerTransformProps(chartProps) as JsonObject; + const layer = createLayerFromData(subsliceCopy, layerProps.payload); setSubSlicesLayers(subSlicesLayers => ({ ...subSlicesLayers, [subsliceCopy.slice_id]: layer, })); - }) - .catch(error => { - throw new Error( - `Error loading layer for slice ${subsliceCopy.slice_id}: ${error}`, - ); }); - } + }) + .catch(error => { + throw new Error( + `Error loading layer for slice ${subsliceCopy.slice_id}: ${error}`, + ); + }); }, - [getLayerIndex, processLayerFilters, createLayerFromData], + [ + getLayerIndex, + processLayerFilters, + createLayerFromData, + props.width, + props.height, + props.datasource, + theme, + ], ); const loadLayers = useCallback( ( formData: QueryFormData, - payload: JsonObject, + slices: ({ slice_id: number } & JsonObject)[], visibleLayers?: number[], ): void => { setViewport(getAdjustedViewport()); @@ -378,7 +419,7 @@ const DeckMulti = (props: DeckMultiProps) => { const deckSlicesOrder = formData.deck_slices || []; - payload.data.slices.forEach( + slices.forEach( (subslice: { slice_id: number } & JsonObject, payloadIndex: number) => { if (visibleDeckLayers && Array.isArray(visibleDeckLayers)) { if (!visibleDeckLayers.includes(subslice.slice_id)) { @@ -391,7 +432,7 @@ const DeckMulti = (props: DeckMultiProps) => { ); const orderedSliceIds = deckSlicesOrder.filter((sliceId: number) => { - const subslice = payload.data.slices.find( + const subslice = slices.find( (s: { slice_id: number }) => s.slice_id === sliceId, ); if (!subslice) return false; @@ -409,6 +450,39 @@ const DeckMulti = (props: DeckMultiProps) => { const prevDeckSlices = usePrevious(props.formData.deck_slices); const prevVisibleLayersRedux = usePrevious(visibleDeckLayersFromRedux); + const fetchSubslices = useCallback( + (sliceIds: number[]) => + Promise.all<({ slice_id: number } & JsonObject) | null>( + sliceIds.map(sliceId => + SupersetClient.get({ endpoint: `/api/v1/chart/${sliceId}` }) + .then(({ json }) => { + const result = (json as JsonObject).result || {}; + let params: JsonObject = {}; + try { + params = JSON.parse(result.params || '{}'); + } catch { + params = {}; + } + return { + slice_id: sliceId, + form_data: { + ...params, + slice_id: sliceId, + viz_type: result.viz_type ?? params.viz_type, + }, + }; + }) + .catch(() => null), + ), + ).then(slices => + slices.filter( + (slice): slice is { slice_id: number } & JsonObject => + slice !== null, + ), + ), + [], + ); + useEffect(() => { const { formData, payload } = props; @@ -419,10 +493,19 @@ const DeckMulti = (props: DeckMultiProps) => { ); if (deckSlicesChanged || visibilityFilterChanged) { - loadLayers(formData, payload, visibleDeckLayersFromRedux); + // legacy explore_json payloads already carried the subslice metadata + const legacySlices = payload?.data?.slices; + if (legacySlices) { + loadLayers(formData, legacySlices, visibleDeckLayersFromRedux); + } else { + fetchSubslices(ensureIsArray(formData.deck_slices) as number[]).then( + slices => loadLayers(formData, slices, visibleDeckLayersFromRedux), + ); + } } }, [ loadLayers, + fetchSubslices, prevDeckSlices, prevVisibleLayersRedux, visibleDeckLayersFromRedux, diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.ts b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.ts new file mode 100644 index 000000000000..f0049857faf0 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.ts @@ -0,0 +1,28 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * The multi-layer chart issues no query of its own: every layer is a + * saved chart fetched client-side through its own buildQuery, exactly + * like the legacy DeckGLMultiLayer whose query_obj was empty. + */ +export default function buildQuery(formData: QueryFormData) { + return buildQueryContext(formData, () => []); +} diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts index 84069b4d9784..133b26f9f858 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts @@ -33,7 +33,6 @@ const metadata = new ChartMetadata({ name: t('deck.gl Multiple Layers'), thumbnail, thumbnailDark, - useLegacyApi: true, tags: [t('deckGL'), t('Multi-Layers')], }); @@ -41,6 +40,7 @@ export default class MultiChartPlugin extends ChartPlugin { constructor() { super({ loadChart: () => import('./Multi'), + loadBuildQuery: () => import('./buildQuery'), controlPanel, metadata, transformProps, diff --git a/tests/unit_tests/charts/data/test_empty_query_context.py b/tests/unit_tests/charts/data/test_empty_query_context.py new file mode 100644 index 000000000000..8392ab8a920d --- /dev/null +++ b/tests/unit_tests/charts/data/test_empty_query_context.py @@ -0,0 +1,43 @@ +# 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. +from typing import Any +from unittest.mock import MagicMock, patch + +from superset.charts.schemas import ChartDataQueryContextSchema + + +def test_query_context_schema_accepts_empty_queries(app_context: Any) -> None: + """ + Charts that compose other charts (deck.gl Multiple Layers) issue no + query of their own: their buildQuery produces an empty ``queries`` + list and every layer is fetched client-side. The schema must accept + that shape so the container chart can flow through /api/v1/chart/data, + and the processor must return an empty result list for it. + """ + with patch( + "superset.common.query_context_factory.DatasourceDAO.get_datasource", + return_value=MagicMock(), + ): + query_context = ChartDataQueryContextSchema().load( + { + "datasource": {"id": 1, "type": "table"}, + "queries": [], + "result_format": "json", + "result_type": "full", + } + ) + assert query_context.queries == [] From c54abcfda8ec960a7bd24624929ede9645e83173 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 08:07:45 -0700 Subject: [PATCH 21/84] feat(compare): migrate compare charts to echarts_timeseries_line and drop the legacy plugin (#41738) Co-authored-by: Claude Code --- .../src/Compare/controlPanel.ts | 76 -- .../src/Compare/images/example-dark.jpg | Bin 63894 -> 0 bytes .../src/Compare/images/example.jpg | Bin 66360 -> 0 bytes .../src/Compare/images/thumbnail-dark.png | Bin 89728 -> 0 bytes .../src/Compare/images/thumbnail.png | Bin 32162 -> 0 bytes .../src/Compare/images/thumbnailLarge.png | Bin 90272 -> 0 bytes .../src/Compare/index.ts | 62 -- .../Compare/stories/NVD3Compare.stories.tsx | 208 ---- .../src/Compare/stories/data.ts | 927 ------------------ .../legacy-preset-chart-nvd3/src/NVD3Vis.ts | 17 +- .../legacy-preset-chart-nvd3/src/index.ts | 1 - .../legacy-preset-chart-nvd3/src/preset.ts | 2 - .../src/visualizations/presets/MainPreset.ts | 2 - superset/cli/viz_migrations.py | 3 + .../shared/migrate_viz/processors.py | 5 + ...b8c9d0_migrate_compare_chart_to_echarts.py | 55 ++ superset/viz.py | 7 - .../viz/nvd3_compare_chart_to_echarts_test.py | 39 + 18 files changed, 103 insertions(+), 1301 deletions(-) delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/controlPanel.ts delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example-dark.jpg delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example.jpg delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnail-dark.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnail.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnailLarge.png delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/index.ts delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/NVD3Compare.stories.tsx delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/data.ts create mode 100644 superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py create mode 100644 tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/controlPanel.ts deleted file mode 100644 index 5a8f8bfb1a1f..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/controlPanel.ts +++ /dev/null @@ -1,76 +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 { t } from '@apache-superset/core/translation'; -import { - ControlPanelConfig, - getStandardizedControls, - sections, -} from '@superset-ui/chart-controls'; -import { - xAxisLabel, - yAxisLabel, - bottomMargin, - xTicksLayout, - xAxisFormat, - yLogScale, - yAxisBounds, - xAxisShowMinmax, - yAxisShowMinmax, - leftMargin, - timeSeriesSection, -} from '../NVD3Controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.legacyTimeseriesTime, - timeSeriesSection[0], - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [['color_scheme']], - }, - { - label: t('X Axis'), - expanded: true, - controlSetRows: [ - [xAxisLabel, bottomMargin], - [xTicksLayout, xAxisFormat], - [xAxisShowMinmax, null], - ], - }, - { - label: t('Y Axis'), - expanded: true, - controlSetRows: [ - [yAxisLabel, leftMargin], - [yAxisShowMinmax, yLogScale], - ['y_axis_format', yAxisBounds], - ], - }, - timeSeriesSection[1], - sections.annotations, - ], - formDataOverrides: formData => ({ - ...formData, - groupby: getStandardizedControls().popAllColumns(), - metrics: getStandardizedControls().popAllMetrics(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example-dark.jpg b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example-dark.jpg deleted file mode 100644 index 658db25a815d08f86c170980ecb1d50d3a4b03c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 63894 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 diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example.jpg b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/example.jpg deleted file mode 100644 index 375ef57a6aab9c58792cb2fbc32934be63bcd9f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66360 zcmeEucU+T8(`W!eMJz}!HhM=&=tZRl2uKN`BOOBT9TY|BN|(?i^n@ZM^di!`l+cTG z>0O%0jqj23>Uq!m-S3v~{&9cb@=IoTW}kU>=b4?Iot@3u$k}JW4Fy?{EC2@w0Kmci z0nTP|MnF(CGG0Kfp}-_ZYOmoA%{Ih$ZBZDBtQPS}TI8%v7CNiF`sS$^Qgf8b(2aCa967i^u! zKX4}vH7P7^ip5zhe!-1@!A%^Te$)@c))BL{asBbEALU1jiOlRE>R5OQ`=JFm1JnR= zfJguAzdZr~5ZVU-@ZS9MnsEXEP#y>XP>%ic8pAsP;Q9*ypse?w*ZwIJ2O}pVEPt$! zu^(J>a{yo`8vwYf4FHgQ0sshg|3Slk{hPMk#gb^T?Xt)ISpaMRW`Mf@5Wo�^q`8 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 PcOF1Q>`-CV|Gx(w_}{+M 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 z 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

>5!q$iB)X(GZhz+u@It^w8Je#+2-e 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 T?oDYS;GdM3yl9#57r*}nw=Z5! diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnail.png b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnail.png deleted file mode 100644 index df29a818d38eb1c16b0f63aa6a18cbe623ee6343..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32162 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ȎnWO3{=cO~W8lPJJpcHuG3LH?TK=H~qOB#i+}Z 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 QveyAwNkxeoapR!>0n%oh#Q*>R diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnailLarge.png b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/images/thumbnailLarge.png deleted file mode 100644 index 86b38069a9b5c590449463a7ca580a0f78aaf0bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90272 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 o*9`^m{k>%D0d3yz$5* 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 import('../ReactNVD3'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/NVD3Compare.stories.tsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/NVD3Compare.stories.tsx deleted file mode 100644 index 86686c90a6ad..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/NVD3Compare.stories.tsx +++ /dev/null @@ -1,208 +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 { CompareChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3'; -import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; -import data from './data'; - -new CompareChartPlugin().configure({ key: VizType.Compare }).register(); - -export default { - title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Compare', - decorators: [withResizableChartDemo], - args: { - colorScheme: 'd3Category10', - contribution: false, - yLogScale: false, - }, - argTypes: { - colorScheme: { - control: 'select', - options: [ - 'supersetColors', - 'd3Category10', - 'bnbColors', - 'googleCategory20c', - ], - }, - contribution: { - control: 'boolean', - description: 'Compute contribution to total', - }, - yLogScale: { control: 'boolean' }, - }, -}; - -export const Basic = ({ - colorScheme, - contribution, - yLogScale, - width, - height, -}: { - colorScheme: string; - contribution: boolean; - yLogScale: boolean; - width: number; - height: number; -}) => ( - -); - -/* eslint-disable no-magic-numbers */ -const timeFormatData = [ - { - key: ['Africa and Middle East'], - values: [ - { x: 1606348800000, y: 3985 }, - { x: 1606435200000, y: 5882 }, - { x: 1606521600000, y: 7983 }, - { x: 1606608000000, y: 16462 }, - { x: 1606694400000, y: 5542 }, - { x: 1606780800000, y: 2825 }, - ], - }, - { - key: ['Asia'], - values: [ - { x: 1606348800000, y: 34837 }, - { x: 1606435200000, y: 40718 }, - { x: 1606521600000, y: 58507 }, - { x: 1606608000000, y: 110120 }, - { x: 1606694400000, y: 43443 }, - { x: 1606780800000, y: 33538 }, - ], - }, - { - key: ['Australia'], - values: [ - { x: 1606348800000, y: 12975 }, - { x: 1606435200000, y: 18471 }, - { x: 1606521600000, y: 17880 }, - { x: 1606608000000, y: 52204 }, - { x: 1606694400000, y: 26690 }, - { x: 1606780800000, y: 16423 }, - ], - }, - { - key: ['Europe'], - values: [ - { x: 1606348800000, y: 127029 }, - { x: 1606435200000, y: 177637 }, - { x: 1606521600000, y: 172653 }, - { x: 1606608000000, y: 203654 }, - { x: 1606694400000, y: 79200 }, - { x: 1606780800000, y: 45238 }, - ], - }, - { - key: ['LatAm'], - values: [ - { x: 1606348800000, y: 22513 }, - { x: 1606435200000, y: 24594 }, - { x: 1606521600000, y: 32578 }, - { x: 1606608000000, y: 34733 }, - { x: 1606694400000, y: 71696 }, - { x: 1606780800000, y: 166611 }, - ], - }, - { - key: ['North America'], - values: [ - { x: 1606348800000, y: 104596 }, - { x: 1606435200000, y: 109850 }, - { x: 1606521600000, y: 136873 }, - { x: 1606608000000, y: 133243 }, - { x: 1606694400000, y: 327739 }, - { x: 1606780800000, y: 162711 }, - ], - }, -]; - -export const timeFormat = () => ( - -); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/data.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/data.ts deleted file mode 100644 index 5d43d8e4bec2..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/stories/data.ts +++ /dev/null @@ -1,927 +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 */ -export default [ - { - key: ['Christopher'], - values: [ - { - x: -157766400000.0, - y: 24703, - }, - { - x: -126230400000.0, - y: 27861, - }, - { - x: -94694400000.0, - y: 29436, - }, - { - x: -63158400000.0, - y: 31463, - }, - { - x: -31536000000.0, - y: 35718, - }, - { - x: 0.0, - y: 41758, - }, - { - x: 31536000000.0, - y: 48172, - }, - { - x: 63072000000.0, - y: 52092, - }, - { - x: 94694400000.0, - y: 48217, - }, - { - x: 126230400000.0, - y: 48476, - }, - { - x: 157766400000.0, - y: 46438, - }, - { - x: 189302400000.0, - y: 45086, - }, - { - x: 220924800000.0, - y: 46610, - }, - { - x: 252460800000.0, - y: 47107, - }, - { - x: 283996800000.0, - y: 50514, - }, - { - x: 315532800000.0, - y: 48969, - }, - { - x: 347155200000.0, - y: 50108, - }, - { - x: 378691200000.0, - y: 59055, - }, - { - x: 410227200000.0, - y: 59188, - }, - { - x: 441763200000.0, - y: 59859, - }, - { - x: 473385600000.0, - y: 59516, - }, - { - x: 504921600000.0, - y: 56633, - }, - { - x: 536457600000.0, - y: 54466, - }, - { - x: 567993600000.0, - y: 52996, - }, - { - x: 599616000000.0, - y: 53205, - }, - { - x: 631152000000.0, - y: 52322, - }, - { - x: 662688000000.0, - y: 47109, - }, - { - x: 694224000000.0, - y: 42470, - }, - { - x: 725846400000.0, - y: 38257, - }, - { - x: 757382400000.0, - y: 34823, - }, - { - x: 788918400000.0, - y: 32728, - }, - { - x: 820454400000.0, - y: 30988, - }, - { - x: 852076800000.0, - y: 29179, - }, - { - x: 883612800000.0, - y: 27083, - }, - { - x: 915148800000.0, - y: 25700, - }, - { - x: 946684800000.0, - y: 24959, - }, - { - x: 978307200000.0, - y: 23180, - }, - { - x: 1009843200000.0, - y: 21731, - }, - { - x: 1041379200000.0, - y: 20793, - }, - { - x: 1072915200000.0, - y: 19739, - }, - { - x: 1104537600000.0, - y: 19190, - }, - { - x: 1136073600000.0, - y: 19674, - }, - { - x: 1167609600000.0, - y: 19986, - }, - { - x: 1199145600000.0, - y: 17771, - }, - ], - }, - { - key: ['David'], - values: [ - { - x: -157766400000.0, - y: 67646, - }, - { - x: -126230400000.0, - y: 66207, - }, - { - x: -94694400000.0, - y: 66581, - }, - { - x: -63158400000.0, - y: 63531, - }, - { - x: -31536000000.0, - y: 63502, - }, - { - x: 0.0, - y: 61570, - }, - { - x: 31536000000.0, - y: 52948, - }, - { - x: 63072000000.0, - y: 46218, - }, - { - x: 94694400000.0, - y: 40968, - }, - { - x: 126230400000.0, - y: 41654, - }, - { - x: 157766400000.0, - y: 39019, - }, - { - x: 189302400000.0, - y: 39165, - }, - { - x: 220924800000.0, - y: 40407, - }, - { - x: 252460800000.0, - y: 40533, - }, - { - x: 283996800000.0, - y: 41898, - }, - { - x: 315532800000.0, - y: 41743, - }, - { - x: 347155200000.0, - y: 40486, - }, - { - x: 378691200000.0, - y: 40283, - }, - { - x: 410227200000.0, - y: 39048, - }, - { - x: 441763200000.0, - y: 38346, - }, - { - x: 473385600000.0, - y: 38395, - }, - { - x: 504921600000.0, - y: 37021, - }, - { - x: 536457600000.0, - y: 36672, - }, - { - x: 567993600000.0, - y: 35214, - }, - { - x: 599616000000.0, - y: 35139, - }, - { - x: 631152000000.0, - y: 33661, - }, - { - x: 662688000000.0, - y: 30347, - }, - { - x: 694224000000.0, - y: 28344, - }, - { - x: 725846400000.0, - y: 26947, - }, - { - x: 757382400000.0, - y: 24784, - }, - { - x: 788918400000.0, - y: 22967, - }, - { - x: 820454400000.0, - y: 22941, - }, - { - x: 852076800000.0, - y: 21824, - }, - { - x: 883612800000.0, - y: 20816, - }, - { - x: 915148800000.0, - y: 20267, - }, - { - x: 946684800000.0, - y: 19695, - }, - { - x: 978307200000.0, - y: 19281, - }, - { - x: 1009843200000.0, - y: 18600, - }, - { - x: 1041379200000.0, - y: 18557, - }, - { - x: 1072915200000.0, - y: 18315, - }, - { - x: 1104537600000.0, - y: 18017, - }, - { - x: 1136073600000.0, - y: 17510, - }, - { - x: 1167609600000.0, - y: 17400, - }, - { - x: 1199145600000.0, - y: 16049, - }, - ], - }, - { - key: ['James'], - values: [ - { - x: -157766400000.0, - y: 67506, - }, - { - x: -126230400000.0, - y: 65036, - }, - { - x: -94694400000.0, - y: 61554, - }, - { - x: -63158400000.0, - y: 60584, - }, - { - x: -31536000000.0, - y: 59824, - }, - { - x: 0.0, - y: 61597, - }, - { - x: 31536000000.0, - y: 54463, - }, - { - x: 63072000000.0, - y: 46960, - }, - { - x: 94694400000.0, - y: 42782, - }, - { - x: 126230400000.0, - y: 41258, - }, - { - x: 157766400000.0, - y: 39471, - }, - { - x: 189302400000.0, - y: 38203, - }, - { - x: 220924800000.0, - y: 39916, - }, - { - x: 252460800000.0, - y: 39783, - }, - { - x: 283996800000.0, - y: 39237, - }, - { - x: 315532800000.0, - y: 39185, - }, - { - x: 347155200000.0, - y: 38176, - }, - { - x: 378691200000.0, - y: 38750, - }, - { - x: 410227200000.0, - y: 36228, - }, - { - x: 441763200000.0, - y: 35728, - }, - { - x: 473385600000.0, - y: 35750, - }, - { - x: 504921600000.0, - y: 33955, - }, - { - x: 536457600000.0, - y: 32552, - }, - { - x: 567993600000.0, - y: 32418, - }, - { - x: 599616000000.0, - y: 32658, - }, - { - x: 631152000000.0, - y: 32288, - }, - { - x: 662688000000.0, - y: 30460, - }, - { - x: 694224000000.0, - y: 28450, - }, - { - x: 725846400000.0, - y: 26193, - }, - { - x: 757382400000.0, - y: 24706, - }, - { - x: 788918400000.0, - y: 22691, - }, - { - x: 820454400000.0, - y: 21122, - }, - { - x: 852076800000.0, - y: 20368, - }, - { - x: 883612800000.0, - y: 19651, - }, - { - x: 915148800000.0, - y: 18508, - }, - { - x: 946684800000.0, - y: 17939, - }, - { - x: 978307200000.0, - y: 17023, - }, - { - x: 1009843200000.0, - y: 16905, - }, - { - x: 1041379200000.0, - y: 16832, - }, - { - x: 1072915200000.0, - y: 16459, - }, - { - x: 1104537600000.0, - y: 16046, - }, - { - x: 1136073600000.0, - y: 16139, - }, - { - x: 1167609600000.0, - y: 15821, - }, - { - x: 1199145600000.0, - y: 14920, - }, - ], - }, - { - key: ['John'], - values: [ - { - x: -157766400000.0, - y: 71390, - }, - { - x: -126230400000.0, - y: 64858, - }, - { - x: -94694400000.0, - y: 61480, - }, - { - x: -63158400000.0, - y: 60754, - }, - { - x: -31536000000.0, - y: 58644, - }, - { - x: 0.0, - y: 58348, - }, - { - x: 31536000000.0, - y: 51382, - }, - { - x: 63072000000.0, - y: 43028, - }, - { - x: 94694400000.0, - y: 39061, - }, - { - x: 126230400000.0, - y: 37553, - }, - { - x: 157766400000.0, - y: 34970, - }, - { - x: 189302400000.0, - y: 33876, - }, - { - x: 220924800000.0, - y: 34103, - }, - { - x: 252460800000.0, - y: 33895, - }, - { - x: 283996800000.0, - y: 35305, - }, - { - x: 315532800000.0, - y: 35131, - }, - { - x: 347155200000.0, - y: 34761, - }, - { - x: 378691200000.0, - y: 34560, - }, - { - x: 410227200000.0, - y: 33047, - }, - { - x: 441763200000.0, - y: 32484, - }, - { - x: 473385600000.0, - y: 31397, - }, - { - x: 504921600000.0, - y: 30103, - }, - { - x: 536457600000.0, - y: 29462, - }, - { - x: 567993600000.0, - y: 29301, - }, - { - x: 599616000000.0, - y: 29751, - }, - { - x: 631152000000.0, - y: 29011, - }, - { - x: 662688000000.0, - y: 27727, - }, - { - x: 694224000000.0, - y: 26156, - }, - { - x: 725846400000.0, - y: 24918, - }, - { - x: 757382400000.0, - y: 24119, - }, - { - x: 788918400000.0, - y: 23174, - }, - { - x: 820454400000.0, - y: 22104, - }, - { - x: 852076800000.0, - y: 21330, - }, - { - x: 883612800000.0, - y: 20556, - }, - { - x: 915148800000.0, - y: 20280, - }, - { - x: 946684800000.0, - y: 20032, - }, - { - x: 978307200000.0, - y: 18839, - }, - { - x: 1009843200000.0, - y: 17400, - }, - { - x: 1041379200000.0, - y: 17170, - }, - { - x: 1072915200000.0, - y: 16381, - }, - { - x: 1104537600000.0, - y: 15692, - }, - { - x: 1136073600000.0, - y: 15083, - }, - { - x: 1167609600000.0, - y: 14348, - }, - { - x: 1199145600000.0, - y: 13110, - }, - ], - }, - { - key: ['Michael'], - values: [ - { - x: -157766400000.0, - y: 80812, - }, - { - x: -126230400000.0, - y: 79709, - }, - { - x: -94694400000.0, - y: 82204, - }, - { - x: -63158400000.0, - y: 81785, - }, - { - x: -31536000000.0, - y: 84893, - }, - { - x: 0.0, - y: 85015, - }, - { - x: 31536000000.0, - y: 77321, - }, - { - x: 63072000000.0, - y: 71197, - }, - { - x: 94694400000.0, - y: 67598, - }, - { - x: 126230400000.0, - y: 67304, - }, - { - x: 157766400000.0, - y: 68149, - }, - { - x: 189302400000.0, - y: 66686, - }, - { - x: 220924800000.0, - y: 67344, - }, - { - x: 252460800000.0, - y: 66875, - }, - { - x: 283996800000.0, - y: 67473, - }, - { - x: 315532800000.0, - y: 68375, - }, - { - x: 347155200000.0, - y: 68467, - }, - { - x: 378691200000.0, - y: 67904, - }, - { - x: 410227200000.0, - y: 67708, - }, - { - x: 441763200000.0, - y: 67457, - }, - { - x: 473385600000.0, - y: 64667, - }, - { - x: 504921600000.0, - y: 63959, - }, - { - x: 536457600000.0, - y: 63442, - }, - { - x: 567993600000.0, - y: 63924, - }, - { - x: 599616000000.0, - y: 65233, - }, - { - x: 631152000000.0, - y: 65138, - }, - { - x: 662688000000.0, - y: 60646, - }, - { - x: 694224000000.0, - y: 54216, - }, - { - x: 725846400000.0, - y: 49443, - }, - { - x: 757382400000.0, - y: 44361, - }, - { - x: 788918400000.0, - y: 41311, - }, - { - x: 820454400000.0, - y: 38284, - }, - { - x: 852076800000.0, - y: 37459, - }, - { - x: 883612800000.0, - y: 36525, - }, - { - x: 915148800000.0, - y: 33820, - }, - { - x: 946684800000.0, - y: 31956, - }, - { - x: 978307200000.0, - y: 29612, - }, - { - x: 1009843200000.0, - y: 28156, - }, - { - x: 1041379200000.0, - y: 27031, - }, - { - x: 1072915200000.0, - y: 25418, - }, - { - x: 1104537600000.0, - y: 23678, - }, - { - x: 1136073600000.0, - y: 22498, - }, - { - x: 1167609600000.0, - y: 21805, - }, - { - x: 1199145600000.0, - y: 20271, - }, - ], - }, -]; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts index 0531a8d869b3..319c43fce510 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts @@ -47,7 +47,6 @@ import { cleanColorInput, computeYDomain, drawBarValues, - generateCompareTooltipContent, generateTimePivotTooltip, generateTooltipClassName, getMaxLabelSize, @@ -124,7 +123,7 @@ const BREAKPOINTS = { small: 340, }; -const TIMESERIES_VIZ_TYPES = [VizType.Compare, VizType.TimePivot]; +const TIMESERIES_VIZ_TYPES = [VizType.TimePivot]; const CHART_ID_PREFIX = 'chart-id-'; @@ -182,7 +181,6 @@ const propTypes = { VizType.BoxPlot, 'bubble', VizType.Bullet, - VizType.Compare, 'column', VizType.TimePivot, 'pie', @@ -399,13 +397,6 @@ function nvd3Vis(element, props) { chart = nv.models.multiBarChart().reduceXTicks(false); break; - case VizType.Compare: - chart = nv.models.cumulativeLineChart(); - chart.xScale(d3.time.scale.utc()); - chart.useInteractiveGuideline(true); - chart.xAxis.showMaxMin(false); - break; - case VizType.BoxPlot: colorKey = 'label'; chart = nv.models.boxPlotChart(); @@ -553,12 +544,6 @@ function nvd3Vis(element, props) { ); } - if (isVizTypes([VizType.Compare])) { - chart.interactiveLayer.tooltip.contentGenerator(d => - generateCompareTooltipContent(d, yAxisFormatter), - ); - } - // This is needed for correct chart dimensions if a chart is rendered in a hidden container chart.width(width); chart.height(height); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts index eb77d57c6a36..e49f353cb951 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts @@ -19,5 +19,4 @@ export { default as NVD3ChartPreset } from './preset'; export { default as BulletChartPlugin } from './Bullet'; -export { default as CompareChartPlugin } from './Compare'; export { default as TimePivotChartPlugin } from './TimePivot'; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts index 584579488fcd..19f918092dfe 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts @@ -18,7 +18,6 @@ */ import { Preset, VizType } from '@superset-ui/core'; import BulletChartPlugin from './Bullet'; -import CompareChartPlugin from './Compare'; import TimePivotChartPlugin from './TimePivot'; export default class NVD3ChartPreset extends Preset { @@ -27,7 +26,6 @@ export default class NVD3ChartPreset extends Preset { name: 'NVD3 charts', plugins: [ new BulletChartPlugin().configure({ key: VizType.Bullet }), - new CompareChartPlugin().configure({ key: VizType.Compare }), new TimePivotChartPlugin().configure({ key: VizType.TimePivot }), ], }); diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index fc76bb907a09..c7e3e9f6e34c 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -35,7 +35,6 @@ import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; import WorldMapChartPlugin from '@superset-ui/legacy-plugin-chart-world-map'; import { BulletChartPlugin, - CompareChartPlugin, TimePivotChartPlugin, } from '@superset-ui/legacy-preset-chart-nvd3'; import { DeckGLChartPreset } from '@superset-ui/preset-chart-deckgl'; @@ -116,7 +115,6 @@ export default class MainPreset extends Preset { new BulletChartPlugin().configure({ key: VizType.Bullet }), new CalendarChartPlugin().configure({ key: VizType.Calendar }), new ChordChartPlugin().configure({ key: VizType.Chord }), - new CompareChartPlugin().configure({ key: VizType.Compare }), new CountryMapChartPlugin().configure({ key: VizType.CountryMap }), new EchartsFunnelChartPlugin().configure({ key: VizType.Funnel }), new EchartsSankeyChartPlugin().configure({ key: VizType.Sankey }), diff --git a/superset/cli/viz_migrations.py b/superset/cli/viz_migrations.py index df62a5985849..793142a425c0 100644 --- a/superset/cli/viz_migrations.py +++ b/superset/cli/viz_migrations.py @@ -33,6 +33,7 @@ MigrateAreaChart, MigrateBarChart, MigrateBubbleChart, + MigrateCompareChart, MigrateDistBarChart, MigrateDualLine, MigrateHeatmapChart, @@ -50,6 +51,7 @@ class VizType(str, Enum): AREA = "area" BAR = "bar" BUBBLE = "bubble" + COMPARE = "compare" DIST_BAR = "dist_bar" DUAL_LINE = "dual_line" HEATMAP = "heatmap" @@ -65,6 +67,7 @@ class VizType(str, Enum): VizType.AREA: MigrateAreaChart, VizType.BAR: MigrateBarChart, VizType.BUBBLE: MigrateBubbleChart, + VizType.COMPARE: MigrateCompareChart, VizType.DIST_BAR: MigrateDistBarChart, VizType.DUAL_LINE: MigrateDualLine, VizType.HEATMAP: MigrateHeatmapChart, diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py index c60b6bd42fcf..8a1a931cff8d 100644 --- a/superset/migrations/shared/migrate_viz/processors.py +++ b/superset/migrations/shared/migrate_viz/processors.py @@ -388,6 +388,11 @@ def _build_query(self) -> dict[str, Any]: return super()._build_query() +class MigrateCompareChart(TimeseriesChart): + source_viz_type = "compare" + target_viz_type = "echarts_timeseries_line" + + class MigrateAreaChart(TimeseriesChart): source_viz_type = "area" target_viz_type = "echarts_area" diff --git a/superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py new file mode 100644 index 000000000000..35766d84dbcc --- /dev/null +++ b/superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py @@ -0,0 +1,55 @@ +# 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. +"""migrate_compare_chart_to_echarts + +Revision ID: e5f6a7b8c9d0 +Revises: d4e5f6a7b8c9 +Create Date: 2026-07-02 21:30:00.000000 + +""" + +from alembic import op +from sqlalchemy import text +from sqlalchemy.dialects.mysql.base import MySQLDialect + +from superset import db +from superset.migrations.shared.migrate_viz import MigrateCompareChart + +# revision identifiers, used by Alembic. +revision = "e5f6a7b8c9d0" +down_revision = "d4e5f6a7b8c9" + + +def upgrade() -> None: + bind = op.get_bind() + + # Ensure `slice.params` and `slice.query_context` in MySQL is MEDIUMTEXT + # before migration, as the migration will save a duplicate form_data backup + # which may significantly increase the size of these fields. + if isinstance(bind.dialect, MySQLDialect): + # If the columns are already MEDIUMTEXT, this is a no-op + op.execute(text("ALTER TABLE slices MODIFY params MEDIUMTEXT")) + op.execute(text("ALTER TABLE slices MODIFY query_context MEDIUMTEXT")) + + session = db.Session(bind=bind) + MigrateCompareChart.upgrade(session) + + +def downgrade() -> None: + bind = op.get_bind() + session = db.Session(bind=bind) + MigrateCompareChart.downgrade(session) diff --git a/superset/viz.py b/superset/viz.py index 56e6001f2b7e..dddb8e698430 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -1190,13 +1190,6 @@ def get_data(self, df: pd.DataFrame) -> VizData: return chart_data -class NVD3CompareTimeSeriesViz(NVD3TimeSeriesViz): - """A line chart component where you can compare the % change over time""" - - viz_type = "compare" - verbose_name = _("Time Series - Percent Change") - - class ChordViz(BaseViz): """A Chord diagram""" diff --git a/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py b/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py new file mode 100644 index 000000000000..6107efc44fd0 --- /dev/null +++ b/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py @@ -0,0 +1,39 @@ +# 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. +from typing import Any + +from superset.migrations.shared.migrate_viz import MigrateCompareChart +from tests.unit_tests.migrations.viz.utils import ( + migrate_and_assert, + TIMESERIES_SOURCE_FORM_DATA, + TIMESERIES_TARGET_FORM_DATA, +) + +SOURCE_FORM_DATA: dict[str, Any] = { + "viz_type": "compare", +} + +TARGET_FORM_DATA: dict[str, Any] = { + "form_data_bak": SOURCE_FORM_DATA, + "viz_type": "echarts_timeseries_line", +} + + +def test_migration() -> None: + SOURCE_FORM_DATA.update(TIMESERIES_SOURCE_FORM_DATA) + TARGET_FORM_DATA.update(TIMESERIES_TARGET_FORM_DATA) + migrate_and_assert(MigrateCompareChart, SOURCE_FORM_DATA, TARGET_FORM_DATA) From 3dfe193ca0a0dfc2224041011f278487b5159511 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 3 Jul 2026 09:15:59 -0700 Subject: [PATCH 22/84] =?UTF-8?q?chore(viz):=20update=20SCOPE=20tracker=20?= =?UTF-8?q?=E2=80=94=20all=20charts=20migrated,=20phases=202-3=20in=20revi?= =?UTF-8?q?ew?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- SCOPE.md | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/SCOPE.md b/SCOPE.md index 8ccf6b6a8e04..971f3bd9f0f8 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -48,10 +48,10 @@ this branch is the EOL. | 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ✅ (merged) | | 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ✅ (merged) | | 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | ✅ (merged) | -| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | 🔃 (#41728) | -| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | 🔃 (staged on #41728) | -| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | 🔃 (#41729) | -| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | 🔃 (#41730) | +| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | ✅ (merged) | +| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | ✅ (merged) | +| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | ✅ (merged) | +| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | ✅ (merged) | "Convert in place" = keep the renderer and the `viz_type` key, add a modern `buildQuery.ts`, move the `viz.py` `get_data()` reshape into `transformProps.ts`, @@ -76,17 +76,17 @@ Per-chart checklist: - [ ] For retargets: `MigrateViz` processor + `superset/cli/viz_migrations.py` registration + Alembic revision (with MySQL MEDIUMTEXT widening) + migration tests - [ ] UPDATES.md entry if user-facing behavior changes -### Phase 2 — Backend removal -- [ ] Remove `explore_json` / `explore_json_data` / `generate_json` from `superset/views/core.py` (incl. CSV/XLSX/async branches) -- [ ] Remove CSRF exemption (`superset/config.py`) and GLOBAL_ASYNC_QUERIES explore_json path -- [ ] Delete `superset/viz.py`; unwind importers: `views/utils.py` (`get_viz`), `models/slice.py` (`Slice.viz`), `commands/chart/warm_up_cache.py`, `common/query_context_processor.py` (annotation path), `security/manager.py` (`BaseViz` overload) -- [ ] Delete `tests/unit_tests/test_viz_*.py`, `tests/integration_tests/viz_tests.py` -- [ ] Frontend plumbing: remove `useLegacyApi` from `ChartMetadata`, `shouldUseLegacyApi`, `/explore_json/` branches in `exploreUtils`, `chartAction.ts`, `ChartClient.ts`, `StatefulChart.tsx`, `DrillByModal.tsx` +### Phase 2 — Backend removal (#41750) +- [x] Remove `explore_json` / `explore_json_data` / `generate_json` from `superset/views/core.py` (incl. CSV/XLSX/async branches) +- [x] Remove CSRF exemption (`superset/config.py`), the `load_explore_json_into_cache` celery task and async manager plumbing +- [x] Delete `superset/viz.py`; unwind importers: `views/utils.py` (`get_viz` + explore-cache perm checkers), `models/slice.py` (`Slice.viz`, `explore_json_url`), `commands/chart/warm_up_cache.py`, `common/query_context_processor.py` (annotation path), `security/manager.py` (`BaseViz` overload + public-role `can_explore_json` grant) +- [x] Delete `tests/unit_tests/test_viz_*.py`, `tests/integration_tests/viz_tests.py`; retarget/remove other explore_json test coverage +- [x] Frontend plumbing: remove `useLegacyApi` from `ChartMetadata`, `shouldUseLegacyApi`, `/explore_json/` branches in `exploreUtils`, `chartAction.ts`, `ChartClient.ts`, `StatefulChart.tsx`, `DrillByModal.tsx` -### Phase 3 — Renames (folded into per-chart PRs where possible) -- [ ] `legacy-plugin-chart-*` → `plugin-chart-*` (dirs, npm names, `file:` deps, MainPreset imports, Storybook/docs) -- [ ] Resolve what remains of `legacy-preset-chart-nvd3` (bullet, time_pivot survivors) -- [ ] Drop "(legacy)" suffixes from user-facing chart names +### Phase 3 — Renames (#41751) +- [x] `legacy-plugin-chart-*` → `plugin-chart-*` (dirs, npm names, `file:` deps, MainPreset imports, Storybook/docs) +- [x] `legacy-preset-chart-nvd3` → `preset-chart-nvd3` (bullet, time_pivot survive there) +- [x] "(legacy)" chart names and Legacy gallery tags removed with each chart migration ## Log of work completed @@ -109,15 +109,14 @@ Per-chart checklist: pipeline; bot caught the legacy 'absolute'→'difference' comparison-type mismatch), #41727 `time_pivot` (pandas offset rollback ported to JS, incl. quarters) merged. All 11 convert-in-place charts are migrated. -- Open PRs: #41728 `bubble` retarget (adds the never-shipped Alembic revision - for MigrateBubbleChart; first attempt chained off the wrong alembic head — - filename timestamps lie), #41729 `partition` (full hierarchy port incl. - period analysis), #41730 `deck_multi` (fully client-side; container issues - an empty-queries context, layers fetched via their own buildQuery + - /api/v1/chart/data), #41732 horizon/rose default-ordering parity fix. - `compare` → echarts_timeseries_line is staged on the bubble branch - (migration revision chains after bubble's) and its PR opens when #41728 - merges. +- 2026-07-03 — #41728 `bubble`, #41729 `partition`, #41730 `deck_multi`, + #41732 ordering parity, #41738 `compare` merged. **All 15 legacy charts + are migrated.** Notable: bubble's revision initially chained off the wrong + alembic head (filename timestamps lie — walk the down_revision graph); + the world bank example now ships `bubble_v2`. +- Open PRs: #41750 Phase 2 (explore_json + viz.py + frontend useLegacyApi + plumbing removal), #41751 Phase 3 (legacy- prefix renames: + plugin-chart-*, preset-chart-nvd3). - 2026-07-02 — Phase 0: removed orphaned `viz.py` classes (`MapboxViz`, `MapLibreViz`, `EventFlowViz`, −204 lines) and the unregistered nvd3 BoxPlot story leftovers. From 1771f05d334c271b44b25917da139a3dcf2ed68f Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 09:36:12 -0700 Subject: [PATCH 23/84] chore(viz): drop the legacy- prefix from migrated chart plugin packages (#41751) Co-authored-by: Claude Code --- .github/CODEOWNERS | 2 +- .../configuration/country-map-tools.mdx | 8 +- superset-frontend/package-lock.json | 239 ------------------ superset-frontend/package.json | 22 +- .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/Calendar.ts | 0 .../src/ReactCalendar.tsx | 0 .../src/buildQuery.ts | 6 +- .../src/controlPanel.ts | 0 .../src/images/example-dark.jpg | Bin .../src/images/example.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../LegacyPluginChartCalendar.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | 0 .../src/transformProps.ts | 7 +- .../src/utils.ts | 0 .../src/vendor/cal-heatmap.ts | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../test/utils.test.ts | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/Chord.ts | 0 .../src/ReactChord.tsx | 0 .../src/buildQuery.ts | 8 +- .../src/controlPanel.ts | 0 .../src/images/chord-dark.jpg | Bin .../src/images/chord.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../LegacyPluginChartChord.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformProps.ts | 0 .../test/buildQuery.test.ts | 0 .../test/transformProps.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../Country Map GeoJSON Generator.ipynb | 0 .../src/CountryMap.ts | 0 .../src/ReactCountryMap.tsx | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.ts | 0 .../src/countries.ts | 0 .../src/countries/afghanistan.geojson | 0 .../src/countries/aland.geojson | 0 .../src/countries/albania.geojson | 0 .../src/countries/algeria.geojson | 0 .../src/countries/american_samoa.geojson | 0 .../src/countries/andorra.geojson | 0 .../src/countries/angola.geojson | 0 .../src/countries/anguilla.geojson | 0 .../src/countries/antarctica.geojson | 0 .../src/countries/antigua_and_barbuda.geojson | 0 .../src/countries/argentina.geojson | 0 .../src/countries/armenia.geojson | 0 .../src/countries/australia.geojson | 0 .../src/countries/austria.geojson | 0 .../src/countries/azerbaijan.geojson | 0 .../src/countries/bahrain.geojson | 0 .../src/countries/bangladesh.geojson | 0 .../src/countries/barbados.geojson | 0 .../src/countries/belarus.geojson | 0 .../src/countries/belgium.geojson | 0 .../src/countries/belize.geojson | 0 .../src/countries/benin.geojson | 0 .../src/countries/bermuda.geojson | 0 .../src/countries/bhutan.geojson | 0 .../src/countries/bolivia.geojson | 0 .../countries/bosnia_and_herzegovina.geojson | 0 .../src/countries/botswana.geojson | 0 .../src/countries/brazil.geojson | 0 .../src/countries/brunei.geojson | 0 .../src/countries/bulgaria.geojson | 0 .../src/countries/burkina_faso.geojson | 0 .../src/countries/burundi.geojson | 0 .../src/countries/cambodia.geojson | 0 .../src/countries/cameroon.geojson | 0 .../src/countries/canada.geojson | 0 .../src/countries/cape_verde.geojson | 0 .../central_african_republic.geojson | 0 .../src/countries/chad.geojson | 0 .../src/countries/chile.geojson | 0 .../src/countries/china.geojson | 0 .../src/countries/colombia.geojson | 0 .../src/countries/comoros.geojson | 0 .../src/countries/cook_islands.geojson | 0 .../src/countries/costa_rica.geojson | 0 .../src/countries/croatia.geojson | 0 .../src/countries/cuba.geojson | 0 .../src/countries/cyprus.geojson | 0 .../src/countries/czech_republic.geojson | 0 .../democratic_republic_of_the_congo.geojson | 0 .../src/countries/denmark.geojson | 0 .../src/countries/djibouti.geojson | 0 .../src/countries/dominica.geojson | 0 .../src/countries/dominican_republic.geojson | 0 .../src/countries/ecuador.geojson | 0 .../src/countries/egypt.geojson | 0 .../src/countries/el_salvador.geojson | 0 .../src/countries/equatorial_guinea.geojson | 0 .../src/countries/eritrea.geojson | 0 .../src/countries/estonia.geojson | 0 .../src/countries/ethiopia.geojson | 0 .../src/countries/fiji.geojson | 0 .../src/countries/finland.geojson | 0 .../src/countries/france.geojson | 0 .../src/countries/france_overseas.geojson | 0 .../src/countries/france_regions.geojson | 0 .../src/countries/french_polynesia.geojson | 0 .../src/countries/gabon.geojson | 0 .../src/countries/gambia.geojson | 0 .../src/countries/germany.geojson | 0 .../src/countries/ghana.geojson | 0 .../src/countries/greece.geojson | 0 .../src/countries/greenland.geojson | 0 .../src/countries/grenada.geojson | 0 .../src/countries/guatemala.geojson | 0 .../src/countries/guinea.geojson | 0 .../src/countries/guyana.geojson | 0 .../src/countries/haiti.geojson | 0 .../src/countries/honduras.geojson | 0 .../src/countries/hungary.geojson | 0 .../src/countries/iceland.geojson | 0 .../src/countries/india.geojson | 0 .../src/countries/indonesia.geojson | 0 .../src/countries/iran.geojson | 0 .../src/countries/israel.geojson | 0 .../src/countries/italy.geojson | 0 .../src/countries/italy_regions.geojson | 0 .../src/countries/ivory_coast.geojson | 0 .../src/countries/japan.geojson | 0 .../src/countries/jordan.geojson | 0 .../src/countries/kazakhstan.geojson | 0 .../src/countries/kenya.geojson | 0 .../src/countries/korea.geojson | 0 .../src/countries/kuwait.geojson | 0 .../src/countries/kyrgyzstan.geojson | 0 .../src/countries/laos.geojson | 0 .../src/countries/latvia.geojson | 0 .../src/countries/lebanon.geojson | 0 .../src/countries/lesotho.geojson | 0 .../src/countries/liberia.geojson | 0 .../src/countries/libya.geojson | 0 .../src/countries/liechtenstein.geojson | 0 .../src/countries/lithuania.geojson | 0 .../src/countries/luxembourg.geojson | 0 .../src/countries/macedonia.geojson | 0 .../src/countries/madagascar.geojson | 0 .../src/countries/malawi.geojson | 0 .../src/countries/malaysia.geojson | 0 .../src/countries/maldives.geojson | 0 .../src/countries/mali.geojson | 0 .../src/countries/malta.geojson | 0 .../src/countries/marshall_islands.geojson | 0 .../src/countries/mauritania.geojson | 0 .../src/countries/mauritius.geojson | 0 .../src/countries/mexico.geojson | 0 .../src/countries/moldova.geojson | 0 .../src/countries/mongolia.geojson | 0 .../src/countries/montenegro.geojson | 0 .../src/countries/montserrat.geojson | 0 .../src/countries/morocco.geojson | 0 .../src/countries/mozambique.geojson | 0 .../src/countries/myanmar.geojson | 0 .../src/countries/namibia.geojson | 0 .../src/countries/nauru.geojson | 0 .../src/countries/nepal.geojson | 0 .../src/countries/netherlands.geojson | 0 .../src/countries/new_caledonia.geojson | 0 .../src/countries/new_zealand.geojson | 0 .../src/countries/nicaragua.geojson | 0 .../src/countries/niger.geojson | 0 .../src/countries/nigeria.geojson | 0 .../northern_mariana_islands.geojson | 0 .../src/countries/norway.geojson | 0 .../src/countries/oman.geojson | 0 .../src/countries/pakistan.geojson | 0 .../src/countries/palau.geojson | 0 .../src/countries/panama.geojson | 0 .../src/countries/papua_new_guinea.geojson | 0 .../src/countries/paraguay.geojson | 0 .../src/countries/peru.geojson | 0 .../src/countries/philippines.geojson | 0 .../src/countries/philippines_regions.geojson | 0 .../src/countries/poland.geojson | 0 .../src/countries/portugal.geojson | 0 .../src/countries/qatar.geojson | 0 .../src/countries/republic_of_serbia.geojson | 0 .../src/countries/romania.geojson | 0 .../src/countries/russia.geojson | 0 .../src/countries/rwanda.geojson | 0 .../src/countries/saint_lucia.geojson | 0 .../saint_pierre_and_miquelon.geojson | 0 .../saint_vincent_and_the_grenadines.geojson | 0 .../src/countries/samoa.geojson | 0 .../src/countries/san_marino.geojson | 0 .../countries/sao_tome_and_principe.geojson | 0 .../src/countries/saudi_arabia.geojson | 0 .../src/countries/senegal.geojson | 0 .../src/countries/seychelles.geojson | 0 .../src/countries/sierra_leone.geojson | 0 .../src/countries/singapore.geojson | 0 .../src/countries/slovakia.geojson | 0 .../src/countries/slovenia.geojson | 0 .../src/countries/solomon_islands.geojson | 0 .../src/countries/somalia.geojson | 0 .../src/countries/south_africa.geojson | 0 .../src/countries/spain.geojson | 0 .../src/countries/sri_lanka.geojson | 0 .../src/countries/sudan.geojson | 0 .../src/countries/suriname.geojson | 0 .../src/countries/sweden.geojson | 0 .../src/countries/switzerland.geojson | 0 .../src/countries/syria.geojson | 0 .../src/countries/taiwan.geojson | 0 .../src/countries/tajikistan.geojson | 0 .../src/countries/tanzania.geojson | 0 .../src/countries/thailand.geojson | 0 .../src/countries/the_bahamas.geojson | 0 .../src/countries/timorleste.geojson | 0 .../src/countries/togo.geojson | 0 .../src/countries/tonga.geojson | 0 .../src/countries/trinidad_and_tobago.geojson | 0 .../src/countries/tunisia.geojson | 0 .../src/countries/turkey.geojson | 0 .../src/countries/turkey_regions.geojson | 0 .../src/countries/turkmenistan.geojson | 0 .../turks_and_caicos_islands.geojson | 0 .../src/countries/uganda.geojson | 0 .../src/countries/uk.geojson | 0 .../src/countries/ukraine.geojson | 0 .../countries/united_arab_emirates.geojson | 0 ...ited_states_minor_outlying_islands.geojson | 0 .../united_states_virgin_islands.geojson | 0 .../src/countries/uruguay.geojson | 0 .../src/countries/usa.geojson | 0 .../src/countries/uzbekistan.geojson | 0 .../src/countries/vanuatu.geojson | 0 .../src/countries/venezuela.geojson | 0 .../src/countries/vietnam.geojson | 0 .../src/countries/wallis_and_futuna.geojson | 0 .../src/countries/yemen.geojson | 0 .../src/countries/zambia.geojson | 0 .../src/countries/zimbabwe.geojson | 0 .../src/geojson.d.ts | 0 .../src/images/exampleGermany-dark.jpg | Bin .../src/images/exampleGermany.jpg | Bin .../src/images/exampleUsa-dark.jpg | Bin .../src/images/exampleUsa.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../src/stories/CountryMap.stories.tsx | 4 +- .../src/transformProps.ts | 0 .../test/CountryMap.test.tsx | 0 .../test/buildQuery.test.ts | 0 .../test/transformProps.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/HorizonChart.tsx | 0 .../src/HorizonRow.tsx | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.ts | 0 .../src/images/Horizon_Chart-dark.jpg | Bin .../src/images/Horizon_Chart.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../src/stories/Horizon.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | Bin .../src/transformProps.ts | 0 .../test/HorizonChart.test.tsx | 0 .../test/HorizonRow.test.tsx | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/PairedTTest.tsx | 0 .../src/TTestTable.tsx | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.ts | 0 .../src/images/example-dark.jpg | Bin .../src/images/example.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/index.ts | 0 .../src/stories/TTest.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | 0 .../src/transformProps.ts | 0 .../test/TTestTable.test.tsx | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/ParallelCoordinates.ts | 0 .../src/ReactParallelCoordinates.tsx | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.ts | 0 .../src/images/example1-dark.jpg | Bin .../src/images/example1.jpg | Bin .../src/images/example2-dark.jpg | Bin .../src/images/example2.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../stories/ParallelCoordinates.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformProps.ts | 0 .../src/vendor/parcoords/d3.parcoords.ts | 0 .../src/vendor/parcoords/divgrid.ts | 0 .../test/buildQuery.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/d3-parcoords.d.ts | 0 .../types/d3.d.ts | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/OptionDescription.tsx | 0 .../src/Partition.ts | 0 .../src/ReactPartition.tsx | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.tsx | 0 .../src/images/example-dark.jpg | Bin .../src/images/example.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../src/stories/Partition.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | 0 .../src/transformProps.ts | 0 .../test/OptionDescription.test.tsx | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/ReactRose.tsx | 0 .../src/Rose.ts | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.tsx | 0 .../src/images/example1-dark.jpg | Bin .../src/images/example1.jpg | Bin .../src/images/example2-dark.jpg | Bin .../src/images/example2.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../src/stories/Rose.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | 0 .../src/transformProps.ts | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 8 +- .../package.json | 6 +- .../src/ReactWorldMap.tsx | 0 .../src/WorldMap.ts | 0 .../src/buildQuery.ts | 0 .../src/controlPanel.ts | 0 .../src/countries.ts | 0 .../src/images/WorldMap1-dark.jpg | Bin .../src/images/WorldMap1.jpg | Bin .../src/images/WorldMap2-dark.jpg | Bin .../src/images/WorldMap2.jpg | Bin .../src/images/thumbnail-dark.png | Bin .../src/images/thumbnail.png | Bin .../src/images/thumbnailLarge.png | Bin .../src/index.ts | 0 .../src/stories/WorldMap.stories.tsx | 4 +- .../src/stories/data.ts | 0 .../src/transformData.ts | 0 .../src/transformProps.ts | 0 .../src/utils.ts | 0 .../test/WorldMap.test.ts | 0 .../test/buildQuery.test.ts | 0 .../test/transformData.test.ts | 0 .../test/transformProps.test.ts | 0 .../test/tsconfig.json | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../CHANGELOG.md | 0 .../README.md | 10 +- .../package.json | 6 +- .../src/Bullet/buildQuery.ts | 0 .../src/Bullet/controlPanel.ts | 0 .../src/Bullet/images/example-dark.jpg | Bin .../src/Bullet/images/example.jpg | Bin .../src/Bullet/images/thumbnail-dark.png | Bin .../src/Bullet/images/thumbnail.png | Bin .../src/Bullet/images/thumbnailLarge.png | Bin .../src/Bullet/index.ts | 0 .../src/Bullet/stories/NVD3Bullet.stories.tsx | 4 +- .../src/Bullet/stories/data.ts | 0 .../src/NVD3Controls.tsx | 0 .../src/NVD3Vis.ts | 0 .../src/PropTypes.ts | 0 .../src/ReactNVD3.tsx | 0 .../src/TimePivot/buildQuery.ts | 0 .../src/TimePivot/controlPanel.ts | 0 .../src/TimePivot/images/example-dark.jpg | Bin .../src/TimePivot/images/example.jpg | Bin .../src/TimePivot/images/thumbnail-dark.png | Bin .../src/TimePivot/images/thumbnail.png | Bin .../src/TimePivot/images/thumbnailLarge.png | Bin .../src/TimePivot/index.ts | 0 .../src/TimePivot/transformData.ts | 0 .../src/TimePivot/transformProps.ts | 0 .../src/index.ts | 0 .../src/preset.ts | 0 .../src/transformProps.ts | 0 .../src/utils.ts | 0 .../src/utils/isTruthy.ts | 0 .../src/utils/tokenize.ts | 0 .../src/vendor/superset/AnnotationTypes.ts | 0 .../test/Bullet.test.ts | 0 .../test/TimePivot.test.ts | 0 .../test/tsconfig.json | 0 .../test/utils.test.ts | 0 .../test/utils/isTruthy.test.ts | 0 .../test/utils/tokenize.test.ts | 0 .../tsconfig.json | 0 .../types/external.d.ts | 0 .../scripts/check-custom-rules.js | 5 +- .../scripts/check-storybook-coverage.js | 4 +- .../src/visualizations/presets/MainPreset.ts | 20 +- superset-frontend/tsconfig.json | 24 +- superset-frontend/webpack.config.js | 4 +- 469 files changed, 153 insertions(+), 378 deletions(-) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/README.md (76%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/package.json (81%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/Calendar.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/ReactCalendar.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/buildQuery.ts (94%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/images/example-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/images/example.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/stories/LegacyPluginChartCalendar.stories.tsx (96%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/transformProps.ts (96%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/utils.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/src/vendor/cal-heatmap.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/test/utils.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-calendar => plugin-chart-calendar}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/README.md (77%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/package.json (81%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/Chord.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/ReactChord.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/buildQuery.ts (89%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/images/chord-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/images/chord.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/stories/LegacyPluginChartChord.stories.tsx (94%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/test/transformProps.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-chord => plugin-chart-chord}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/README.md (78%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/package.json (79%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/scripts/Country Map GeoJSON Generator.ipynb (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/CountryMap.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/ReactCountryMap.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/afghanistan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/aland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/albania.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/algeria.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/american_samoa.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/andorra.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/angola.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/anguilla.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/antarctica.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/antigua_and_barbuda.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/argentina.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/armenia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/australia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/austria.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/azerbaijan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bahrain.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bangladesh.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/barbados.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/belarus.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/belgium.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/belize.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/benin.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bermuda.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bhutan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bolivia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bosnia_and_herzegovina.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/botswana.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/brazil.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/brunei.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/bulgaria.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/burkina_faso.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/burundi.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cambodia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cameroon.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/canada.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cape_verde.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/central_african_republic.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/chad.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/chile.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/china.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/colombia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/comoros.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cook_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/costa_rica.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/croatia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cuba.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/cyprus.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/czech_republic.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/democratic_republic_of_the_congo.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/denmark.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/djibouti.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/dominica.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/dominican_republic.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/ecuador.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/egypt.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/el_salvador.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/equatorial_guinea.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/eritrea.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/estonia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/ethiopia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/fiji.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/finland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/france.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/france_overseas.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/france_regions.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/french_polynesia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/gabon.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/gambia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/germany.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/ghana.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/greece.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/greenland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/grenada.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/guatemala.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/guinea.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/guyana.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/haiti.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/honduras.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/hungary.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/iceland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/india.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/indonesia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/iran.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/israel.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/italy.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/italy_regions.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/ivory_coast.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/japan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/jordan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/kazakhstan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/kenya.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/korea.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/kuwait.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/kyrgyzstan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/laos.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/latvia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/lebanon.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/lesotho.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/liberia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/libya.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/liechtenstein.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/lithuania.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/luxembourg.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/macedonia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/madagascar.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/malawi.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/malaysia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/maldives.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mali.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/malta.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/marshall_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mauritania.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mauritius.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mexico.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/moldova.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mongolia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/montenegro.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/montserrat.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/morocco.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/mozambique.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/myanmar.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/namibia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/nauru.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/nepal.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/netherlands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/new_caledonia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/new_zealand.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/nicaragua.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/niger.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/nigeria.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/northern_mariana_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/norway.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/oman.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/pakistan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/palau.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/panama.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/papua_new_guinea.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/paraguay.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/peru.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/philippines.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/philippines_regions.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/poland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/portugal.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/qatar.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/republic_of_serbia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/romania.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/russia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/rwanda.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/saint_lucia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/saint_pierre_and_miquelon.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/saint_vincent_and_the_grenadines.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/samoa.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/san_marino.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/sao_tome_and_principe.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/saudi_arabia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/senegal.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/seychelles.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/sierra_leone.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/singapore.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/slovakia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/slovenia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/solomon_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/somalia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/south_africa.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/spain.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/sri_lanka.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/sudan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/suriname.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/sweden.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/switzerland.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/syria.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/taiwan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/tajikistan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/tanzania.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/thailand.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/the_bahamas.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/timorleste.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/togo.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/tonga.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/trinidad_and_tobago.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/tunisia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/turkey.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/turkey_regions.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/turkmenistan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/turks_and_caicos_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/uganda.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/uk.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/ukraine.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/united_arab_emirates.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/united_states_minor_outlying_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/united_states_virgin_islands.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/uruguay.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/usa.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/uzbekistan.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/vanuatu.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/venezuela.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/vietnam.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/wallis_and_futuna.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/yemen.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/zambia.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/countries/zimbabwe.geojson (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/geojson.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/exampleGermany-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/exampleGermany.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/exampleUsa-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/exampleUsa.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/stories/CountryMap.stories.tsx (96%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/test/CountryMap.test.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/test/transformProps.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-country-map => plugin-chart-country-map}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/README.md (76%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/package.json (80%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/HorizonChart.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/HorizonRow.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/images/Horizon_Chart-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/images/Horizon_Chart.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/stories/Horizon.stories.tsx (93%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/test/HorizonChart.test.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/test/HorizonRow.test.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-horizon => plugin-chart-horizon}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/README.md (75%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/package.json (79%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/PairedTTest.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/TTestTable.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/images/example-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/images/example.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/stories/TTest.stories.tsx (93%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/test/TTestTable.test.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-paired-t-test => plugin-chart-paired-t-test}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/README.md (74%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/package.json (78%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/ParallelCoordinates.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/ReactParallelCoordinates.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/example1-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/example1.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/example2-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/example2.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/stories/ParallelCoordinates.stories.tsx (92%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/vendor/parcoords/d3.parcoords.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/src/vendor/parcoords/divgrid.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/types/d3-parcoords.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/types/d3.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-parallel-coordinates => plugin-chart-parallel-coordinates}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/README.md (76%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/package.json (82%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/OptionDescription.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/Partition.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/ReactPartition.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/controlPanel.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/images/example-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/images/example.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/stories/Partition.stories.tsx (95%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/test/OptionDescription.test.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-partition => plugin-chart-partition}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/README.md (77%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/package.json (82%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/ReactRose.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/Rose.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/controlPanel.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/example1-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/example1.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/example2-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/example2.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/stories/Rose.stories.tsx (95%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-rose => plugin-chart-rose}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/README.md (76%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/package.json (82%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/ReactWorldMap.tsx (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/WorldMap.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/countries.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/WorldMap1-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/WorldMap1.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/WorldMap2-dark.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/WorldMap2.jpg (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/stories/WorldMap.stories.tsx (92%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/transformData.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/src/utils.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/test/WorldMap.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/test/buildQuery.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/test/transformData.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/test/transformProps.test.ts (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-plugin-chart-world-map => plugin-chart-world-map}/types/external.d.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/CHANGELOG.md (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/README.md (78%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/package.json (85%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/images/example-dark.jpg (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/images/example.jpg (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/index.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/stories/NVD3Bullet.stories.tsx (94%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/Bullet/stories/data.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/NVD3Controls.tsx (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/NVD3Vis.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/PropTypes.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/ReactNVD3.tsx (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/buildQuery.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/controlPanel.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/images/example-dark.jpg (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/images/example.jpg (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/images/thumbnail-dark.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/images/thumbnail.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/images/thumbnailLarge.png (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/index.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/transformData.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/TimePivot/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/index.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/preset.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/transformProps.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/utils.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/utils/isTruthy.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/utils/tokenize.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/src/vendor/superset/AnnotationTypes.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/Bullet.test.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/TimePivot.test.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/utils.test.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/utils/isTruthy.test.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/test/utils/tokenize.test.ts (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/tsconfig.json (100%) rename superset-frontend/plugins/{legacy-preset-chart-nvd3 => preset-chart-nvd3}/types/external.d.ts (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bbfada4582d8..34512702bc73 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -34,7 +34,7 @@ **/*.geojson @villebro @rusackas **/*.ipynb @villebro @rusackas -/superset-frontend/plugins/legacy-plugin-chart-country-map/ @villebro @rusackas +/superset-frontend/plugins/plugin-chart-country-map/ @villebro @rusackas # Notify translation maintainers of changes to translations diff --git a/docs/admin_docs/configuration/country-map-tools.mdx b/docs/admin_docs/configuration/country-map-tools.mdx index ae128ed3b7c1..10a9fcee15b7 100644 --- a/docs/admin_docs/configuration/country-map-tools.mdx +++ b/docs/admin_docs/configuration/country-map-tools.mdx @@ -18,7 +18,7 @@ code is less ambiguous and is unique to all regions in the world. ## Included Maps The current list of countries can be found in the src -[legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts) +[plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts) The Country Maps visualization already ships with the maps for the following countries: @@ -31,10 +31,10 @@ The Country Maps visualization already ships with the maps for the following cou ## Adding a New Country To add a new country to the list, you'd have to edit files in -[@superset-ui/legacy-plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-country-map). +[@superset-ui/plugin-chart-country-map](https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map). -1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb). -2. Edit the countries list in [legacy-plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts). +1. Generate a new GeoJSON file for your country following the guide in [this Jupyter notebook](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/scripts/Country%20Map%20GeoJSON%20Generator.ipynb). +2. Edit the countries list in [plugin-chart-country-map/src/countries.ts](https://github.com/apache/superset/blob/master/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts). 3. Install superset-frontend dependencies: `cd superset-frontend && npm install` 4. Verify your countries in Superset plugins storybook: `npm run plugins:storybook`. 5. Build and install Superset from source code. diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index bc1d311d81e4..08ba786868f0 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -44184,245 +44184,6 @@ "version": "0.20.3", "license": "Apache-2.0" }, - "plugins/legacy-plugin-chart-calendar": { - "name": "@superset-ui/legacy-plugin-chart-calendar", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3-array": "^3.2.4", - "d3-selection": "^3.0.0", - "d3-tip": "^0.9.1", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@emotion/react": "^11.4.1", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-calendar/node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "plugins/legacy-plugin-chart-calendar/node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "plugins/legacy-plugin-chart-chord": { - "name": "@superset-ui/legacy-plugin-chart-chord", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "prop-types": "^15.8.1", - "react": "^19.2.7" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*" - } - }, - "plugins/legacy-plugin-chart-chord/node_modules/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", - "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "plugins/legacy-plugin-chart-country-map": { - "name": "@superset-ui/legacy-plugin-chart-country-map", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "d3-array": "^3.2.4", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-country-map/node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "plugins/legacy-plugin-chart-horizon": { - "name": "@superset-ui/legacy-plugin-chart-horizon", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3-array": "^3.2.4", - "d3-scale": "^4.0.2", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-horizon/node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "plugins/legacy-plugin-chart-paired-t-test": { - "name": "@superset-ui/legacy-plugin-chart-paired-t-test", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "distributions": "^2.2.0", - "prop-types": "^15.8.1", - "reactable": "^1.1.0" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-parallel-coordinates": { - "name": "@superset-ui/legacy-plugin-chart-parallel-coordinates", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3v3": "npm:d3@3.5.17", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-partition": { - "name": "@superset-ui/legacy-plugin-chart-partition", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "d3-hierarchy": "^3.1.2", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "@testing-library/jest-dom": "*", - "@testing-library/react": "^14.0.0", - "react": "^18.3.0", - "react-dom": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-rose": { - "name": "@superset-ui/legacy-plugin-chart-rose", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "nvd3-fork": "^2.0.5", - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@emotion/react": "^11.4.1", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-world-map": { - "name": "@superset-ui/legacy-plugin-chart-world-map", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "d3-array": "^3.2.4", - "datamaps": "^0.5.10", - "prop-types": "^15.8.1", - "tinycolor2": "^1.6.0" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "react": "^18.3.0" - } - }, - "plugins/legacy-plugin-chart-world-map/node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "plugins/legacy-preset-chart-nvd3": { - "name": "@superset-ui/legacy-preset-chart-nvd3", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "d3-tip": "^0.9.1", - "dompurify": "^3.4.12", - "fast-safe-stringify": "^2.1.1", - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", - "nvd3-fork": "^2.0.5", - "prop-types": "^15.8.1", - "urijs": "^1.19.11" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "dayjs": "^1.11.21", - "react": "^18.3.0" - } - }, "plugins/plugin-chart-ag-grid-table": { "name": "@superset-ui/plugin-chart-ag-grid-table", "version": "0.20.3", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index f4cbd22aa23a..0d6c4f4e7c6a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -89,7 +89,7 @@ "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80% --silent", "test-loud": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max-old-space-size=8192\" jest --max-workers=80%", "type": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" tsc --noEmit", - "update-maps": "cd plugins/legacy-plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'", + "update-maps": "cd plugins/plugin-chart-country-map/scripts && jupyter nbconvert --to notebook --execute --inplace --allow-errors --ExecutePreprocessor.timeout=1200 'Country Map GeoJSON Generator.ipynb'", "validate-release": "../RELEASING/validate_this_release.sh" }, "browserslist": [ @@ -136,16 +136,16 @@ "@scarf/scarf": "^1.4.0", "@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls", "@superset-ui/core": "file:./packages/superset-ui-core", - "@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar", - "@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord", - "@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map", - "@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon", - "@superset-ui/legacy-plugin-chart-paired-t-test": "file:./plugins/legacy-plugin-chart-paired-t-test", - "@superset-ui/legacy-plugin-chart-parallel-coordinates": "file:./plugins/legacy-plugin-chart-parallel-coordinates", - "@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition", - "@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose", - "@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map", - "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3", + "@superset-ui/plugin-chart-calendar": "file:./plugins/plugin-chart-calendar", + "@superset-ui/plugin-chart-chord": "file:./plugins/plugin-chart-chord", + "@superset-ui/plugin-chart-country-map": "file:./plugins/plugin-chart-country-map", + "@superset-ui/plugin-chart-horizon": "file:./plugins/plugin-chart-horizon", + "@superset-ui/plugin-chart-paired-t-test": "file:./plugins/plugin-chart-paired-t-test", + "@superset-ui/plugin-chart-parallel-coordinates": "file:./plugins/plugin-chart-parallel-coordinates", + "@superset-ui/plugin-chart-partition": "file:./plugins/plugin-chart-partition", + "@superset-ui/plugin-chart-rose": "file:./plugins/plugin-chart-rose", + "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", + "@superset-ui/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3", "@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table", "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-calendar/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-calendar/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/README.md b/superset-frontend/plugins/plugin-chart-calendar/README.md similarity index 76% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/README.md rename to superset-frontend/plugins/plugin-chart-calendar/README.md index f1dea584cf8d..d1a927ee20ad 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/README.md +++ b/superset-frontend/plugins/plugin-chart-calendar/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-calendar +## @superset-ui/plugin-chart-calendar -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-calendar.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-calendar) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-calendar?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-calendar) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-calendar.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-calendar) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-calendar?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-calendar) This plugin provides Calendar Heatmap for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar'; +import CalendarChartPlugin from '@superset-ui/plugin-chart-calendar'; new CalendarChartPlugin().configure({ key: 'calendar' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json b/superset-frontend/plugins/plugin-chart-calendar/package.json similarity index 81% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/package.json rename to superset-frontend/plugins/plugin-chart-calendar/package.json index 31bc492155f4..9767e1b91197 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/package.json +++ b/superset-frontend/plugins/plugin-chart-calendar/package.json @@ -1,18 +1,18 @@ { - "name": "@superset-ui/legacy-plugin-chart-calendar", + "name": "@superset-ui/plugin-chart-calendar", "version": "0.20.3", "description": "Superset Legacy Chart - Calendar Heatmap", "keywords": [ "superset" ], - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-calendar#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-calendar#readme", "bugs": { "url": "https://github.com/apache/superset/issues" }, "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-calendar" + "directory": "superset-frontend/plugins/plugin-chart-calendar" }, "license": "Apache-2.0", "author": "Superset", diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.ts b/superset-frontend/plugins/plugin-chart-calendar/src/Calendar.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/Calendar.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.tsx b/superset-frontend/plugins/plugin-chart-calendar/src/ReactCalendar.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.tsx rename to superset-frontend/plugins/plugin-chart-calendar/src/ReactCalendar.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-calendar/src/buildQuery.ts similarity index 94% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/buildQuery.ts index 29fb5eaf783c..43acb041dec0 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/buildQuery.ts +++ b/superset-frontend/plugins/plugin-chart-calendar/src/buildQuery.ts @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { buildQueryContext, QueryFormData, TimeGranularity } from '@superset-ui/core'; +import { + buildQueryContext, + QueryFormData, + TimeGranularity, +} from '@superset-ui/core'; const SUBDOMAIN_TO_TIME_GRAIN: Record = { min: TimeGranularity.MINUTE, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-calendar/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/example-dark.jpg b/superset-frontend/plugins/plugin-chart-calendar/src/images/example-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/example-dark.jpg rename to superset-frontend/plugins/plugin-chart-calendar/src/images/example-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/example.jpg b/superset-frontend/plugins/plugin-chart-calendar/src/images/example.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/example.jpg rename to superset-frontend/plugins/plugin-chart-calendar/src/images/example.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-calendar/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts b/superset-frontend/plugins/plugin-chart-calendar/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx b/superset-frontend/plugins/plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx similarity index 96% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx rename to superset-frontend/plugins/plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx index a2a685e205a9..02a8f12c6b48 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-calendar/src/stories/LegacyPluginChartCalendar.stories.tsx @@ -18,14 +18,14 @@ */ import { SuperChart } from '@superset-ui/core'; -import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar'; +import CalendarChartPlugin from '@superset-ui/plugin-chart-calendar'; import data from './data'; import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; new CalendarChartPlugin().configure({ key: 'calendar' }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-calendar', + title: 'Chart Plugins/plugin-chart-calendar', decorators: [withResizableChartDemo], args: { cellSize: 10, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-calendar/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts b/superset-frontend/plugins/plugin-chart-calendar/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-calendar/src/transformProps.ts similarity index 96% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/transformProps.ts index 30de387c7720..c170832504b2 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-calendar/src/transformProps.ts @@ -53,8 +53,11 @@ export default function transformProps(chartProps: ChartProps) { // The legacy explore_json endpoint computed the per-metric value maps // and domain range server-side; v1 responses arrive as flat records. - const { data: rawData, from_dttm: fromDttm, to_dttm: toDttm } = - queriesData[0]; + const { + data: rawData, + from_dttm: fromDttm, + to_dttm: toDttm, + } = queriesData[0]; const data = Array.isArray(rawData) ? transformData( rawData, diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/utils.ts b/superset-frontend/plugins/plugin-chart-calendar/src/utils.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/utils.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/utils.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.ts b/superset-frontend/plugins/plugin-chart-calendar/src/vendor/cal-heatmap.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.ts rename to superset-frontend/plugins/plugin-chart-calendar/src/vendor/cal-heatmap.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-calendar/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/test/utils.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/utils.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/test/utils.test.ts rename to superset-frontend/plugins/plugin-chart-calendar/test/utils.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json b/superset-frontend/plugins/plugin-chart-calendar/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/tsconfig.json rename to superset-frontend/plugins/plugin-chart-calendar/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts b/superset-frontend/plugins/plugin-chart-calendar/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-calendar/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-chord/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-chord/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/README.md b/superset-frontend/plugins/plugin-chart-chord/README.md similarity index 77% rename from superset-frontend/plugins/legacy-plugin-chart-chord/README.md rename to superset-frontend/plugins/plugin-chart-chord/README.md index 16146f86f643..7cdbf1c25950 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/README.md +++ b/superset-frontend/plugins/plugin-chart-chord/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-chord +## @superset-ui/plugin-chart-chord -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-chord.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-chord) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-chord?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-chord) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-chord.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-chord) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-chord?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-chord) This plugin provides Chord Diagram for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord'; +import ChordChartPlugin from '@superset-ui/plugin-chart-chord'; new ChordChartPlugin().configure({ key: 'chord' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/package.json b/superset-frontend/plugins/plugin-chart-chord/package.json similarity index 81% rename from superset-frontend/plugins/legacy-plugin-chart-chord/package.json rename to superset-frontend/plugins/plugin-chart-chord/package.json index 711153df749c..1f5cf6314492 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/package.json +++ b/superset-frontend/plugins/plugin-chart-chord/package.json @@ -1,5 +1,5 @@ { - "name": "@superset-ui/legacy-plugin-chart-chord", + "name": "@superset-ui/plugin-chart-chord", "version": "0.20.3", "description": "Superset Legacy Chart - Chord Diagram", "sideEffects": [ @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-chord" + "directory": "superset-frontend/plugins/plugin-chart-chord" }, "keywords": [ "superset" @@ -24,7 +24,7 @@ "bugs": { "url": "https://github.com/apache/superset/issues" }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-chord#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-chord#readme", "publishConfig": { "access": "public" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.ts b/superset-frontend/plugins/plugin-chart-chord/src/Chord.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.ts rename to superset-frontend/plugins/plugin-chart-chord/src/Chord.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.tsx b/superset-frontend/plugins/plugin-chart-chord/src/ReactChord.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.tsx rename to superset-frontend/plugins/plugin-chart-chord/src/ReactChord.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-chord/src/buildQuery.ts similarity index 89% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-chord/src/buildQuery.ts index 66e41a5cc018..6c3a2a1c85bd 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/buildQuery.ts +++ b/superset-frontend/plugins/plugin-chart-chord/src/buildQuery.ts @@ -30,8 +30,12 @@ import { */ export default function buildQuery(formData: QueryFormData) { const { groupby, columns, metric, sort_by_metric } = formData; - const source = ensureIsArray(groupby as QueryFormColumn | QueryFormColumn[])[0]; - const target = ensureIsArray(columns as QueryFormColumn | QueryFormColumn[])[0]; + const source = ensureIsArray( + groupby as QueryFormColumn | QueryFormColumn[], + )[0]; + const target = ensureIsArray( + columns as QueryFormColumn | QueryFormColumn[], + )[0]; return buildQueryContext(formData, baseQueryObject => [ { ...baseQueryObject, diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-chord/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-chord/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/images/chord-dark.jpg b/superset-frontend/plugins/plugin-chart-chord/src/images/chord-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/images/chord-dark.jpg rename to superset-frontend/plugins/plugin-chart-chord/src/images/chord-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/images/chord.jpg b/superset-frontend/plugins/plugin-chart-chord/src/images/chord.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/images/chord.jpg rename to superset-frontend/plugins/plugin-chart-chord/src/images/chord.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-chord/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-chord/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-chord/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts b/superset-frontend/plugins/plugin-chart-chord/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts rename to superset-frontend/plugins/plugin-chart-chord/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx b/superset-frontend/plugins/plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx similarity index 94% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx rename to superset-frontend/plugins/plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx index c3cca05572f3..ca0c695be6c3 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-chord/src/stories/LegacyPluginChartChord.stories.tsx @@ -18,14 +18,14 @@ */ import { SuperChart, VizType } from '@superset-ui/core'; -import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord'; +import ChordChartPlugin from '@superset-ui/plugin-chart-chord'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; new ChordChartPlugin().configure({ key: VizType.Chord }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-chord', + title: 'Chart Plugins/plugin-chart-chord', decorators: [withResizableChartDemo], args: { colorScheme: 'd3Category10', diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-chord/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-chord/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-chord/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-chord/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-chord/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-chord/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-chord/test/transformProps.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/test/transformProps.test.ts rename to superset-frontend/plugins/plugin-chart-chord/test/transformProps.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-chord/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-chord/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json b/superset-frontend/plugins/plugin-chart-chord/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/tsconfig.json rename to superset-frontend/plugins/plugin-chart-chord/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts b/superset-frontend/plugins/plugin-chart-chord/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-chord/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-country-map/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-country-map/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/README.md b/superset-frontend/plugins/plugin-chart-country-map/README.md similarity index 78% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/README.md rename to superset-frontend/plugins/plugin-chart-country-map/README.md index df3e2dd19bc1..21b1ba37698b 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/README.md +++ b/superset-frontend/plugins/plugin-chart-country-map/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-country-map +## @superset-ui/plugin-chart-country-map -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-country-map.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-country-map) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-country-map?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-country-map) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-country-map.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-country-map) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-country-map?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-country-map) This plugin provides Country Map for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map'; +import CountryMapChartPlugin from '@superset-ui/plugin-chart-country-map'; new CountryMapChartPlugin().configure({ key: 'country-map' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json b/superset-frontend/plugins/plugin-chart-country-map/package.json similarity index 79% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/package.json rename to superset-frontend/plugins/plugin-chart-country-map/package.json index 704028afc927..00d8eca42095 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/package.json +++ b/superset-frontend/plugins/plugin-chart-country-map/package.json @@ -1,5 +1,5 @@ { - "name": "@superset-ui/legacy-plugin-chart-country-map", + "name": "@superset-ui/plugin-chart-country-map", "version": "0.20.3", "description": "Superset Legacy Chart - Country Map", "main": "lib/index.js", @@ -11,7 +11,7 @@ "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-country-map" + "directory": "superset-frontend/plugins/plugin-chart-country-map" }, "keywords": [ "superset" @@ -21,7 +21,7 @@ "bugs": { "url": "https://github.com/apache/superset/issues" }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-country-map#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-country-map#readme", "publishConfig": { "access": "public" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb b/superset-frontend/plugins/plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb rename to superset-frontend/plugins/plugin-chart-country-map/scripts/Country Map GeoJSON Generator.ipynb diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.ts b/superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/CountryMap.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.tsx b/superset-frontend/plugins/plugin-chart-country-map/src/ReactCountryMap.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.tsx rename to superset-frontend/plugins/plugin-chart-country-map/src/ReactCountryMap.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-country-map/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts b/superset-frontend/plugins/plugin-chart-country-map/src/countries.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/countries.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/afghanistan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/afghanistan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/afghanistan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/afghanistan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/aland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/aland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/aland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/aland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/albania.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/albania.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/albania.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/albania.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/algeria.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/algeria.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/algeria.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/algeria.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/american_samoa.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/american_samoa.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/american_samoa.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/american_samoa.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/andorra.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/andorra.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/andorra.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/andorra.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/angola.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/angola.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/angola.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/angola.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/anguilla.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/anguilla.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/anguilla.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/anguilla.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/antarctica.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/antarctica.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/antarctica.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/antarctica.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/antigua_and_barbuda.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/antigua_and_barbuda.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/antigua_and_barbuda.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/antigua_and_barbuda.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/argentina.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/argentina.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/argentina.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/argentina.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/armenia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/armenia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/armenia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/armenia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/australia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/australia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/australia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/australia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/austria.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/austria.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/austria.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/austria.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/azerbaijan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/azerbaijan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/azerbaijan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/azerbaijan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bahrain.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bahrain.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bahrain.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bahrain.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bangladesh.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bangladesh.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bangladesh.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bangladesh.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/barbados.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/barbados.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/barbados.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/barbados.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belarus.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/belarus.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belarus.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/belarus.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belgium.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/belgium.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belgium.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/belgium.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belize.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/belize.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/belize.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/belize.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/benin.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/benin.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/benin.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/benin.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bermuda.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bermuda.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bermuda.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bermuda.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bhutan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bhutan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bhutan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bhutan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bolivia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bolivia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bolivia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bolivia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bosnia_and_herzegovina.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bosnia_and_herzegovina.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bosnia_and_herzegovina.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bosnia_and_herzegovina.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/botswana.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/botswana.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/botswana.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/botswana.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/brazil.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/brazil.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/brazil.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/brazil.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/brunei.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/brunei.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/brunei.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/brunei.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bulgaria.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/bulgaria.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/bulgaria.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/bulgaria.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/burkina_faso.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/burkina_faso.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/burkina_faso.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/burkina_faso.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/burundi.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/burundi.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/burundi.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/burundi.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cambodia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cambodia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cambodia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cambodia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cameroon.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cameroon.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cameroon.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cameroon.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/canada.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/canada.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/canada.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/canada.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cape_verde.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cape_verde.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cape_verde.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cape_verde.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/central_african_republic.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/central_african_republic.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/central_african_republic.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/central_african_republic.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/chad.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/chad.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/chad.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/chad.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/chile.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/chile.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/chile.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/chile.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/china.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/china.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/china.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/china.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/colombia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/colombia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/colombia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/colombia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/comoros.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/comoros.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/comoros.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/comoros.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cook_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cook_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cook_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cook_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/costa_rica.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/costa_rica.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/costa_rica.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/costa_rica.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/croatia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/croatia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/croatia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/croatia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cuba.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cuba.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cuba.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cuba.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cyprus.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/cyprus.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/cyprus.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/cyprus.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/czech_republic.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/czech_republic.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/czech_republic.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/czech_republic.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/democratic_republic_of_the_congo.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/democratic_republic_of_the_congo.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/democratic_republic_of_the_congo.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/democratic_republic_of_the_congo.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/denmark.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/denmark.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/denmark.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/denmark.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/djibouti.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/djibouti.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/djibouti.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/djibouti.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/dominica.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/dominica.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/dominica.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/dominica.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/dominican_republic.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/dominican_republic.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/dominican_republic.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/dominican_republic.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ecuador.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/ecuador.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ecuador.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/ecuador.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/egypt.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/egypt.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/egypt.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/egypt.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/el_salvador.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/el_salvador.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/el_salvador.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/el_salvador.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/equatorial_guinea.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/equatorial_guinea.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/equatorial_guinea.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/equatorial_guinea.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/eritrea.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/eritrea.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/eritrea.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/eritrea.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/estonia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/estonia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/estonia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/estonia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ethiopia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/ethiopia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ethiopia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/ethiopia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/fiji.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/fiji.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/fiji.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/fiji.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/finland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/finland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/finland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/finland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/france.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/france.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france_overseas.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/france_overseas.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france_overseas.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/france_overseas.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france_regions.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/france_regions.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/france_regions.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/france_regions.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/french_polynesia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/french_polynesia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/french_polynesia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/french_polynesia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/gabon.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/gabon.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/gabon.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/gabon.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/gambia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/gambia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/gambia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/gambia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/germany.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/germany.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/germany.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/germany.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ghana.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/ghana.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ghana.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/ghana.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/greece.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/greece.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/greece.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/greece.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/greenland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/greenland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/greenland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/greenland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/grenada.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/grenada.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/grenada.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/grenada.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guatemala.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/guatemala.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guatemala.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/guatemala.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guinea.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/guinea.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guinea.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/guinea.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guyana.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/guyana.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/guyana.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/guyana.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/haiti.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/haiti.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/haiti.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/haiti.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/honduras.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/honduras.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/honduras.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/honduras.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/hungary.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/hungary.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/hungary.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/hungary.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/iceland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/iceland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/iceland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/iceland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/india.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/india.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/india.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/indonesia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/indonesia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/indonesia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/indonesia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/iran.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/iran.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/iran.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/iran.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/israel.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/israel.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/israel.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/israel.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/italy.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/italy.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/italy.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/italy.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/italy_regions.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/italy_regions.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/italy_regions.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/italy_regions.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ivory_coast.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/ivory_coast.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ivory_coast.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/ivory_coast.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/japan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/japan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/japan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/japan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/jordan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/jordan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/jordan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/jordan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kazakhstan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/kazakhstan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kazakhstan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/kazakhstan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kenya.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/kenya.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kenya.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/kenya.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/korea.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/korea.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/korea.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/korea.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kuwait.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/kuwait.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kuwait.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/kuwait.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kyrgyzstan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/kyrgyzstan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/kyrgyzstan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/kyrgyzstan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/laos.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/laos.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/laos.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/laos.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/latvia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/latvia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/latvia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/latvia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lebanon.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/lebanon.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lebanon.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/lebanon.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lesotho.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/lesotho.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lesotho.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/lesotho.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/liberia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/liberia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/liberia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/liberia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/libya.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/libya.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/libya.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/libya.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/liechtenstein.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/liechtenstein.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/liechtenstein.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/liechtenstein.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lithuania.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/lithuania.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/lithuania.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/lithuania.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/luxembourg.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/luxembourg.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/luxembourg.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/luxembourg.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/macedonia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/macedonia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/macedonia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/macedonia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/madagascar.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/madagascar.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/madagascar.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/madagascar.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malawi.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/malawi.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malawi.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/malawi.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malaysia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/malaysia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malaysia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/malaysia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/maldives.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/maldives.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/maldives.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/maldives.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mali.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mali.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mali.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mali.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malta.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/malta.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/malta.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/malta.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/marshall_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/marshall_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/marshall_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/marshall_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mauritania.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mauritania.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mauritania.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mauritania.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mauritius.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mauritius.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mauritius.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mauritius.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mexico.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mexico.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mexico.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mexico.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/moldova.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/moldova.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/moldova.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/moldova.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mongolia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mongolia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mongolia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mongolia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/montenegro.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/montenegro.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/montenegro.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/montenegro.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/montserrat.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/montserrat.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/montserrat.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/montserrat.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/morocco.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/morocco.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/morocco.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/morocco.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mozambique.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/mozambique.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/mozambique.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/mozambique.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/myanmar.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/myanmar.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/myanmar.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/myanmar.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/namibia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/namibia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/namibia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/namibia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nauru.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/nauru.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nauru.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/nauru.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nepal.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/nepal.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nepal.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/nepal.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/netherlands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/netherlands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/netherlands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/netherlands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/new_caledonia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/new_caledonia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/new_caledonia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/new_caledonia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/new_zealand.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/new_zealand.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/new_zealand.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/new_zealand.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nicaragua.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/nicaragua.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nicaragua.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/nicaragua.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/niger.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/niger.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/niger.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/niger.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nigeria.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/nigeria.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/nigeria.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/nigeria.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/northern_mariana_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/northern_mariana_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/northern_mariana_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/northern_mariana_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/norway.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/norway.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/norway.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/norway.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/oman.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/oman.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/oman.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/oman.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/pakistan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/pakistan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/pakistan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/pakistan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/palau.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/palau.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/palau.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/palau.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/panama.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/panama.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/panama.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/panama.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/papua_new_guinea.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/papua_new_guinea.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/papua_new_guinea.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/papua_new_guinea.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/paraguay.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/paraguay.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/paraguay.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/paraguay.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/peru.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/peru.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/peru.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/peru.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/philippines.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/philippines.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/philippines.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/philippines.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/philippines_regions.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/philippines_regions.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/philippines_regions.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/philippines_regions.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/poland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/poland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/poland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/poland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/portugal.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/portugal.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/portugal.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/portugal.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/qatar.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/qatar.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/qatar.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/qatar.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/republic_of_serbia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/republic_of_serbia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/republic_of_serbia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/republic_of_serbia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/romania.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/romania.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/romania.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/romania.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/russia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/russia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/russia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/russia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/rwanda.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/rwanda.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/rwanda.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/rwanda.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_lucia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_lucia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_lucia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_lucia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_pierre_and_miquelon.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_pierre_and_miquelon.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_pierre_and_miquelon.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_pierre_and_miquelon.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_vincent_and_the_grenadines.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_vincent_and_the_grenadines.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saint_vincent_and_the_grenadines.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/saint_vincent_and_the_grenadines.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/samoa.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/samoa.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/samoa.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/samoa.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/san_marino.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/san_marino.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/san_marino.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/san_marino.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sao_tome_and_principe.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/sao_tome_and_principe.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sao_tome_and_principe.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/sao_tome_and_principe.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saudi_arabia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/saudi_arabia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/saudi_arabia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/saudi_arabia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/senegal.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/senegal.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/senegal.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/senegal.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/seychelles.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/seychelles.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/seychelles.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/seychelles.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sierra_leone.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/sierra_leone.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sierra_leone.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/sierra_leone.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/singapore.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/singapore.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/singapore.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/singapore.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/slovakia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/slovakia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/slovakia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/slovakia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/slovenia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/slovenia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/slovenia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/slovenia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/solomon_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/solomon_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/solomon_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/solomon_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/somalia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/somalia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/somalia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/somalia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/south_africa.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/south_africa.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/south_africa.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/south_africa.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/spain.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/spain.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/spain.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/spain.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sri_lanka.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/sri_lanka.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sri_lanka.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/sri_lanka.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sudan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/sudan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sudan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/sudan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/suriname.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/suriname.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/suriname.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/suriname.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sweden.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/sweden.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/sweden.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/sweden.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/switzerland.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/switzerland.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/switzerland.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/switzerland.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/syria.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/syria.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/syria.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/syria.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/taiwan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/taiwan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/taiwan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/taiwan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tajikistan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/tajikistan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tajikistan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/tajikistan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tanzania.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/tanzania.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tanzania.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/tanzania.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/thailand.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/thailand.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/thailand.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/thailand.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/the_bahamas.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/the_bahamas.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/the_bahamas.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/the_bahamas.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/timorleste.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/timorleste.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/timorleste.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/timorleste.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/togo.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/togo.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/togo.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/togo.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tonga.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/tonga.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tonga.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/tonga.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/trinidad_and_tobago.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/trinidad_and_tobago.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/trinidad_and_tobago.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/trinidad_and_tobago.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tunisia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/tunisia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/tunisia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/tunisia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkey.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/turkey.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkey.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/turkey.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkey_regions.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/turkey_regions.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkey_regions.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/turkey_regions.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkmenistan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/turkmenistan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turkmenistan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/turkmenistan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turks_and_caicos_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/turks_and_caicos_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/turks_and_caicos_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/turks_and_caicos_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uganda.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uganda.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uganda.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/uganda.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uk.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uk.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/uk.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/ukraine.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/ukraine.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/ukraine.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_arab_emirates.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/united_arab_emirates.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_arab_emirates.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/united_arab_emirates.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_states_minor_outlying_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/united_states_minor_outlying_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_states_minor_outlying_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/united_states_minor_outlying_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_states_virgin_islands.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/united_states_virgin_islands.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/united_states_virgin_islands.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/united_states_virgin_islands.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uruguay.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uruguay.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uruguay.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/uruguay.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/usa.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/usa.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/usa.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/usa.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uzbekistan.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/uzbekistan.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/uzbekistan.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/uzbekistan.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/vanuatu.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/vanuatu.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/vanuatu.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/vanuatu.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/venezuela.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/venezuela.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/venezuela.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/venezuela.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/vietnam.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/vietnam.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/vietnam.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/vietnam.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/wallis_and_futuna.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/wallis_and_futuna.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/wallis_and_futuna.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/wallis_and_futuna.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/yemen.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/yemen.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/yemen.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/yemen.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/zambia.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/zambia.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/zambia.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/zambia.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/zimbabwe.geojson b/superset-frontend/plugins/plugin-chart-country-map/src/countries/zimbabwe.geojson similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/countries/zimbabwe.geojson rename to superset-frontend/plugins/plugin-chart-country-map/src/countries/zimbabwe.geojson diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/geojson.d.ts b/superset-frontend/plugins/plugin-chart-country-map/src/geojson.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/geojson.d.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/geojson.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleGermany-dark.jpg b/superset-frontend/plugins/plugin-chart-country-map/src/images/exampleGermany-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleGermany-dark.jpg rename to superset-frontend/plugins/plugin-chart-country-map/src/images/exampleGermany-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleGermany.jpg b/superset-frontend/plugins/plugin-chart-country-map/src/images/exampleGermany.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleGermany.jpg rename to superset-frontend/plugins/plugin-chart-country-map/src/images/exampleGermany.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleUsa-dark.jpg b/superset-frontend/plugins/plugin-chart-country-map/src/images/exampleUsa-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleUsa-dark.jpg rename to superset-frontend/plugins/plugin-chart-country-map/src/images/exampleUsa-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleUsa.jpg b/superset-frontend/plugins/plugin-chart-country-map/src/images/exampleUsa.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/exampleUsa.jpg rename to superset-frontend/plugins/plugin-chart-country-map/src/images/exampleUsa.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-country-map/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts b/superset-frontend/plugins/plugin-chart-country-map/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/stories/CountryMap.stories.tsx b/superset-frontend/plugins/plugin-chart-country-map/src/stories/CountryMap.stories.tsx similarity index 96% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/stories/CountryMap.stories.tsx rename to superset-frontend/plugins/plugin-chart-country-map/src/stories/CountryMap.stories.tsx index e4f46b2bcbe1..04f994a1b4f4 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/stories/CountryMap.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-country-map/src/stories/CountryMap.stories.tsx @@ -22,13 +22,13 @@ import { JsonObject, seed, SuperChart, SequentialD3 } from '@superset-ui/core'; import { useTheme } from '@apache-superset/core/theme'; import CountryMapChartPlugin, { countries, -} from '@superset-ui/legacy-plugin-chart-country-map'; +} from '@superset-ui/plugin-chart-country-map'; import { withResizableChartDemo } from '@storybook-shared'; new CountryMapChartPlugin().configure({ key: 'country-map' }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-country-map', + title: 'Chart Plugins/plugin-chart-country-map', decorators: [withResizableChartDemo], component: SuperChart, parameters: { diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-country-map/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx b/superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx rename to superset-frontend/plugins/plugin-chart-country-map/test/CountryMap.test.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-country-map/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/test/transformProps.test.ts rename to superset-frontend/plugins/plugin-chart-country-map/test/transformProps.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-country-map/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-country-map/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json b/superset-frontend/plugins/plugin-chart-country-map/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/tsconfig.json rename to superset-frontend/plugins/plugin-chart-country-map/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts b/superset-frontend/plugins/plugin-chart-country-map/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-country-map/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-horizon/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-horizon/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/README.md b/superset-frontend/plugins/plugin-chart-horizon/README.md similarity index 76% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/README.md rename to superset-frontend/plugins/plugin-chart-horizon/README.md index 07d30796211c..12fee0d46025 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/README.md +++ b/superset-frontend/plugins/plugin-chart-horizon/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-horizon +## @superset-ui/plugin-chart-horizon -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-horizon.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-horizon) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-horizon?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-horizon) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-horizon.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-horizon) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-horizon?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-horizon) This plugin provides Horizon for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import HorizonChartPlugin from '@superset-ui/legacy-plugin-chart-horizon'; +import HorizonChartPlugin from '@superset-ui/plugin-chart-horizon'; new HorizonChartPlugin().configure({ key: 'horizon' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json b/superset-frontend/plugins/plugin-chart-horizon/package.json similarity index 80% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/package.json rename to superset-frontend/plugins/plugin-chart-horizon/package.json index 8e151752be13..06a906ac8fa1 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/package.json +++ b/superset-frontend/plugins/plugin-chart-horizon/package.json @@ -1,18 +1,18 @@ { - "name": "@superset-ui/legacy-plugin-chart-horizon", + "name": "@superset-ui/plugin-chart-horizon", "version": "0.20.3", "description": "Superset Legacy Chart - Horizon", "keywords": [ "superset" ], - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-horizon#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-horizon#readme", "bugs": { "url": "https://github.com/apache/superset/issues" }, "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/packages/legacy-plugin-chart-horizon" + "directory": "superset-frontend/plugins/plugin-chart-horizon" }, "license": "Apache-2.0", "author": "Superset", diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.tsx b/superset-frontend/plugins/plugin-chart-horizon/src/HorizonChart.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.tsx rename to superset-frontend/plugins/plugin-chart-horizon/src/HorizonChart.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonRow.tsx b/superset-frontend/plugins/plugin-chart-horizon/src/HorizonRow.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonRow.tsx rename to superset-frontend/plugins/plugin-chart-horizon/src/HorizonRow.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-horizon/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-horizon/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/Horizon_Chart-dark.jpg b/superset-frontend/plugins/plugin-chart-horizon/src/images/Horizon_Chart-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/Horizon_Chart-dark.jpg rename to superset-frontend/plugins/plugin-chart-horizon/src/images/Horizon_Chart-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/Horizon_Chart.jpg b/superset-frontend/plugins/plugin-chart-horizon/src/images/Horizon_Chart.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/Horizon_Chart.jpg rename to superset-frontend/plugins/plugin-chart-horizon/src/images/Horizon_Chart.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-horizon/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts b/superset-frontend/plugins/plugin-chart-horizon/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/stories/Horizon.stories.tsx b/superset-frontend/plugins/plugin-chart-horizon/src/stories/Horizon.stories.tsx similarity index 93% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/stories/Horizon.stories.tsx rename to superset-frontend/plugins/plugin-chart-horizon/src/stories/Horizon.stories.tsx index 46582d5fd243..d8105cea7972 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/stories/Horizon.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-horizon/src/stories/Horizon.stories.tsx @@ -18,14 +18,14 @@ */ import { SuperChart, VizType } from '@superset-ui/core'; -import HorizonChartPlugin from '@superset-ui/legacy-plugin-chart-horizon'; +import HorizonChartPlugin from '@superset-ui/plugin-chart-horizon'; import { withResizableChartDemo } from '@storybook-shared'; import data from './data'; new HorizonChartPlugin().configure({ key: VizType.Horizon }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-horizon', + title: 'Chart Plugins/plugin-chart-horizon', decorators: [withResizableChartDemo], args: { horizonColorScale: 'series', diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-horizon/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformData.ts b/superset-frontend/plugins/plugin-chart-horizon/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-horizon/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-horizon/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/HorizonChart.test.tsx b/superset-frontend/plugins/plugin-chart-horizon/test/HorizonChart.test.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/test/HorizonChart.test.tsx rename to superset-frontend/plugins/plugin-chart-horizon/test/HorizonChart.test.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/HorizonRow.test.tsx b/superset-frontend/plugins/plugin-chart-horizon/test/HorizonRow.test.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/test/HorizonRow.test.tsx rename to superset-frontend/plugins/plugin-chart-horizon/test/HorizonRow.test.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-horizon/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-horizon/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-horizon/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-horizon/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json b/superset-frontend/plugins/plugin-chart-horizon/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/tsconfig.json rename to superset-frontend/plugins/plugin-chart-horizon/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts b/superset-frontend/plugins/plugin-chart-horizon/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-horizon/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-paired-t-test/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-paired-t-test/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/README.md b/superset-frontend/plugins/plugin-chart-paired-t-test/README.md similarity index 75% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/README.md rename to superset-frontend/plugins/plugin-chart-paired-t-test/README.md index 34bc5fc4cace..e3ce8313f7a5 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/README.md +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-paired-t-test +## @superset-ui/plugin-chart-paired-t-test -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-paired-t-test.svg?style=flat)](hhttps://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-paired-t-test) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-paired-t-test?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-paired-t-test) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-paired-t-test.svg?style=flat)](hhttps://www.npmjs.com/package/@superset-ui/plugin-chart-paired-t-test) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-paired-t-test?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-paired-t-test) This plugin provides Paired T Test for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import PairedTTestChartPlugin from '@superset-ui/legacy-plugin-chart-paired-t-test'; +import PairedTTestChartPlugin from '@superset-ui/plugin-chart-paired-t-test'; new PairedTTestChartPlugin().configure({ key: 'paired-t-test' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json similarity index 79% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json rename to superset-frontend/plugins/plugin-chart-paired-t-test/package.json index bb910734bf4b..675aa8417c2e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/package.json +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json @@ -1,18 +1,18 @@ { - "name": "@superset-ui/legacy-plugin-chart-paired-t-test", + "name": "@superset-ui/plugin-chart-paired-t-test", "version": "0.20.3", "description": "Superset Legacy Chart - Paired T Test", "keywords": [ "superset" ], - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-paired-t-test#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-paired-t-test#readme", "bugs": { "url": "https://github.com/apache/superset/issues" }, "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-paired-t-test" + "directory": "superset-frontend/plugins/plugin-chart-paired-t-test" }, "license": "Apache-2.0", "author": "Superset", diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/example-dark.jpg b/superset-frontend/plugins/plugin-chart-paired-t-test/src/images/example-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/example-dark.jpg rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/images/example-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/example.jpg b/superset-frontend/plugins/plugin-chart-paired-t-test/src/images/example.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/example.jpg rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/images/example.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/stories/TTest.stories.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/stories/TTest.stories.tsx similarity index 93% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/stories/TTest.stories.tsx rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/stories/TTest.stories.tsx index 449105070e3f..56f00cd454fb 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/stories/TTest.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/stories/TTest.stories.tsx @@ -19,14 +19,14 @@ /* eslint-disable no-magic-numbers */ import { SuperChart } from '@superset-ui/core'; -import PairedTTestChartPlugin from '@superset-ui/legacy-plugin-chart-paired-t-test'; +import PairedTTestChartPlugin from '@superset-ui/plugin-chart-paired-t-test'; import { withResizableChartDemo } from '@storybook-shared'; import data from './data'; new PairedTTestChartPlugin().configure({ key: 'paired-t-test' }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-paired-t-test', + title: 'Chart Plugins/plugin-chart-paired-t-test', decorators: [withResizableChartDemo], args: { liftvaluePrecision: 4, diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/TTestTable.test.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/TTestTable.test.tsx rename to superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-paired-t-test/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-paired-t-test/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json b/superset-frontend/plugins/plugin-chart-paired-t-test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-paired-t-test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-paired-t-test/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-parallel-coordinates/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/README.md b/superset-frontend/plugins/plugin-chart-parallel-coordinates/README.md similarity index 74% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/README.md rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/README.md index 350b9c95a090..e513b62f671f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/README.md +++ b/superset-frontend/plugins/plugin-chart-parallel-coordinates/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-parallel-coordinates +## @superset-ui/plugin-chart-parallel-coordinates -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-parallel-coordinates.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-parallel-coordinates) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-parallel-coordinates?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-parallel-coordinates) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-parallel-coordinates.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-parallel-coordinates) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-parallel-coordinates?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-parallel-coordinates) This plugin provides Parallel Coordinates for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import ParallelCoordinatesChartPlugin from '@superset-ui/legacy-plugin-chart-parallel-coordinates'; +import ParallelCoordinatesChartPlugin from '@superset-ui/plugin-chart-parallel-coordinates'; new ParallelCoordinatesChartPlugin() .configure({ key: 'parallel-coordinates' }) diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json b/superset-frontend/plugins/plugin-chart-parallel-coordinates/package.json similarity index 78% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/package.json index 26915611ea1d..d79976dce472 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/package.json +++ b/superset-frontend/plugins/plugin-chart-parallel-coordinates/package.json @@ -1,5 +1,5 @@ { - "name": "@superset-ui/legacy-plugin-chart-parallel-coordinates", + "name": "@superset-ui/plugin-chart-parallel-coordinates", "version": "0.20.3", "description": "Superset Legacy Chart - Parallel Coordinates", "sideEffects": [ @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates" + "directory": "superset-frontend/plugins/plugin-chart-parallel-coordinates" }, "keywords": [ "superset" @@ -24,7 +24,7 @@ "bugs": { "url": "https://github.com/apache/superset/issues" }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-parallel-coordinates#readme", "publishConfig": { "access": "public" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example1-dark.jpg b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example1-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example1-dark.jpg rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example1-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example1.jpg b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example1.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example1.jpg rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example1.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example2-dark.jpg b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example2-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example2-dark.jpg rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example2-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example2.jpg b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example2.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/example2.jpg rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/example2.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx similarity index 92% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx index c466bc0b091d..4fd46e5c87f1 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/stories/ParallelCoordinates.stories.tsx @@ -18,7 +18,7 @@ */ import { SuperChart } from '@superset-ui/core'; -import ParallelCoordinatesChartPlugin from '@superset-ui/legacy-plugin-chart-parallel-coordinates'; +import ParallelCoordinatesChartPlugin from '@superset-ui/plugin-chart-parallel-coordinates'; import { withResizableChartDemo } from '@storybook-shared'; import data from './data'; @@ -27,7 +27,7 @@ new ParallelCoordinatesChartPlugin() .register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-parallel-coordinates', + title: 'Chart Plugins/plugin-chart-parallel-coordinates', decorators: [withResizableChartDemo], args: { includeSeries: false, diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-parallel-coordinates/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json b/superset-frontend/plugins/plugin-chart-parallel-coordinates/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/tsconfig.json rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/types/d3.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/types/d3.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts b/superset-frontend/plugins/plugin-chart-parallel-coordinates/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-parallel-coordinates/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-partition/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-partition/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/README.md b/superset-frontend/plugins/plugin-chart-partition/README.md similarity index 76% rename from superset-frontend/plugins/legacy-plugin-chart-partition/README.md rename to superset-frontend/plugins/plugin-chart-partition/README.md index e5e07cc1457c..4d1ce9340bf1 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/README.md +++ b/superset-frontend/plugins/plugin-chart-partition/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-partition +## @superset-ui/plugin-chart-partition -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-partition.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-partition) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-partition?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-partition) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-partition.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-partition) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-partition?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-partition) This plugin provides Partition for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import PartitionChartPlugin from '@superset-ui/legacy-plugin-chart-partition'; +import PartitionChartPlugin from '@superset-ui/plugin-chart-partition'; new PartitionChartPlugin().configure({ key: 'partition' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/package.json b/superset-frontend/plugins/plugin-chart-partition/package.json similarity index 82% rename from superset-frontend/plugins/legacy-plugin-chart-partition/package.json rename to superset-frontend/plugins/plugin-chart-partition/package.json index 23ef1d9efa0f..a78458c6e9e4 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/package.json +++ b/superset-frontend/plugins/plugin-chart-partition/package.json @@ -1,18 +1,18 @@ { - "name": "@superset-ui/legacy-plugin-chart-partition", + "name": "@superset-ui/plugin-chart-partition", "version": "0.20.3", "description": "Superset Legacy Chart - Partition", "keywords": [ "superset" ], - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-partition#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-partition#readme", "bugs": { "url": "https://github.com/apache/superset/issues" }, "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/packages/legacy-plugin-chart-partition" + "directory": "superset-frontend/plugins/plugin-chart-partition" }, "license": "Apache-2.0", "author": "Superset", diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/OptionDescription.tsx b/superset-frontend/plugins/plugin-chart-partition/src/OptionDescription.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/OptionDescription.tsx rename to superset-frontend/plugins/plugin-chart-partition/src/OptionDescription.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.ts b/superset-frontend/plugins/plugin-chart-partition/src/Partition.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.ts rename to superset-frontend/plugins/plugin-chart-partition/src/Partition.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.tsx b/superset-frontend/plugins/plugin-chart-partition/src/ReactPartition.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.tsx rename to superset-frontend/plugins/plugin-chart-partition/src/ReactPartition.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-partition/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-partition/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/controlPanel.tsx rename to superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/images/example-dark.jpg b/superset-frontend/plugins/plugin-chart-partition/src/images/example-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/images/example-dark.jpg rename to superset-frontend/plugins/plugin-chart-partition/src/images/example-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/images/example.jpg b/superset-frontend/plugins/plugin-chart-partition/src/images/example.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/images/example.jpg rename to superset-frontend/plugins/plugin-chart-partition/src/images/example.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-partition/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-partition/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-partition/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts b/superset-frontend/plugins/plugin-chart-partition/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts rename to superset-frontend/plugins/plugin-chart-partition/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/stories/Partition.stories.tsx b/superset-frontend/plugins/plugin-chart-partition/src/stories/Partition.stories.tsx similarity index 95% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/stories/Partition.stories.tsx rename to superset-frontend/plugins/plugin-chart-partition/src/stories/Partition.stories.tsx index d4286ea46652..4d35f3b23a18 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/stories/Partition.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-partition/src/stories/Partition.stories.tsx @@ -18,14 +18,14 @@ */ import { SuperChart, VizType } from '@superset-ui/core'; -import PartitionChartPlugin from '@superset-ui/legacy-plugin-chart-partition'; +import PartitionChartPlugin from '@superset-ui/plugin-chart-partition'; import data from './data'; import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; new PartitionChartPlugin().configure({ key: VizType.Partition }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-partition', + title: 'Chart Plugins/plugin-chart-partition', decorators: [withResizableChartDemo], args: { colorScheme: 'd3Category10', diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-partition/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-partition/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts b/superset-frontend/plugins/plugin-chart-partition/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-partition/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-partition/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-partition/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.tsx b/superset-frontend/plugins/plugin-chart-partition/test/OptionDescription.test.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.tsx rename to superset-frontend/plugins/plugin-chart-partition/test/OptionDescription.test.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-partition/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-partition/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-partition/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-partition/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-partition/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-partition/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json b/superset-frontend/plugins/plugin-chart-partition/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/tsconfig.json rename to superset-frontend/plugins/plugin-chart-partition/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts b/superset-frontend/plugins/plugin-chart-partition/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-partition/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-rose/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-rose/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/README.md b/superset-frontend/plugins/plugin-chart-rose/README.md similarity index 77% rename from superset-frontend/plugins/legacy-plugin-chart-rose/README.md rename to superset-frontend/plugins/plugin-chart-rose/README.md index c010abbd7923..91cf643674ad 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/README.md +++ b/superset-frontend/plugins/plugin-chart-rose/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-rose +## @superset-ui/plugin-chart-rose -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-rose.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-rose) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-rose?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-rose) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-rose.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-rose) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-rose?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-rose) This plugin provides Nightingale Rose Diagram for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose'; +import RoseChartPlugin from '@superset-ui/plugin-chart-rose'; new RoseChartPlugin().configure({ key: 'rose' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/package.json b/superset-frontend/plugins/plugin-chart-rose/package.json similarity index 82% rename from superset-frontend/plugins/legacy-plugin-chart-rose/package.json rename to superset-frontend/plugins/plugin-chart-rose/package.json index 725e5b91b8bf..1510e8a2b261 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/package.json +++ b/superset-frontend/plugins/plugin-chart-rose/package.json @@ -1,18 +1,18 @@ { - "name": "@superset-ui/legacy-plugin-chart-rose", + "name": "@superset-ui/plugin-chart-rose", "version": "0.20.3", "description": "Superset Legacy Chart - Nightingale Rose Diagram", "keywords": [ "superset" ], - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-rose#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-rose#readme", "bugs": { "url": "https://github.com/apache/superset/issues" }, "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-rose" + "directory": "superset-frontend/plugins/plugin-chart-rose" }, "license": "Apache-2.0", "author": "Superset", diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.tsx b/superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.tsx rename to superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.ts b/superset-frontend/plugins/plugin-chart-rose/src/Rose.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.ts rename to superset-frontend/plugins/plugin-chart-rose/src/Rose.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-rose/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-rose/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-rose/src/controlPanel.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx rename to superset-frontend/plugins/plugin-chart-rose/src/controlPanel.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1-dark.jpg b/superset-frontend/plugins/plugin-chart-rose/src/images/example1-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1-dark.jpg rename to superset-frontend/plugins/plugin-chart-rose/src/images/example1-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1.jpg b/superset-frontend/plugins/plugin-chart-rose/src/images/example1.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example1.jpg rename to superset-frontend/plugins/plugin-chart-rose/src/images/example1.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2-dark.jpg b/superset-frontend/plugins/plugin-chart-rose/src/images/example2-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2-dark.jpg rename to superset-frontend/plugins/plugin-chart-rose/src/images/example2-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2.jpg b/superset-frontend/plugins/plugin-chart-rose/src/images/example2.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/example2.jpg rename to superset-frontend/plugins/plugin-chart-rose/src/images/example2.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-rose/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-rose/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-rose/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-rose/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-rose/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-rose/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts b/superset-frontend/plugins/plugin-chart-rose/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts rename to superset-frontend/plugins/plugin-chart-rose/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/stories/Rose.stories.tsx b/superset-frontend/plugins/plugin-chart-rose/src/stories/Rose.stories.tsx similarity index 95% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/stories/Rose.stories.tsx rename to superset-frontend/plugins/plugin-chart-rose/src/stories/Rose.stories.tsx index 3140635752ee..3bec652a6189 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/stories/Rose.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-rose/src/stories/Rose.stories.tsx @@ -19,14 +19,14 @@ /* eslint-disable no-magic-numbers, sort-keys */ import { SuperChart, VizType } from '@superset-ui/core'; -import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose'; +import RoseChartPlugin from '@superset-ui/plugin-chart-rose'; import data from './data'; import { withResizableChartDemo } from '@storybook-shared'; new RoseChartPlugin().configure({ key: VizType.Rose }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-rose', + title: 'Chart Plugins/plugin-chart-rose', decorators: [withResizableChartDemo], args: { colorScheme: 'd3Category10', diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-rose/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-rose/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts b/superset-frontend/plugins/plugin-chart-rose/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-rose/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-rose/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-rose/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-rose/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-rose/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-rose/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-rose/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json b/superset-frontend/plugins/plugin-chart-rose/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/tsconfig.json rename to superset-frontend/plugins/plugin-chart-rose/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts b/superset-frontend/plugins/plugin-chart-rose/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-rose/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/CHANGELOG.md b/superset-frontend/plugins/plugin-chart-world-map/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/CHANGELOG.md rename to superset-frontend/plugins/plugin-chart-world-map/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/README.md b/superset-frontend/plugins/plugin-chart-world-map/README.md similarity index 76% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/README.md rename to superset-frontend/plugins/plugin-chart-world-map/README.md index b9965f6b26e9..574dd6ae6452 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/README.md +++ b/superset-frontend/plugins/plugin-chart-world-map/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-plugin-chart-world-map +## @superset-ui/plugin-chart-world-map -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-plugin-chart-world-map.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-plugin-chart-world-map) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-plugin-chart-world-map?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-plugin-chart-world-map) +[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-world-map.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/plugin-chart-world-map) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fplugin-chart-world-map?style=flat)](https://libraries.io/npm/@superset-ui%2Fplugin-chart-world-map) This plugin provides World Map for Superset. @@ -30,7 +30,7 @@ Configure `key`, which can be any `string`, and register the plugin. This `key` lookup this chart throughout the app. ```js -import WorldmapChartPlugin from '@superset-ui/legacy-plugin-chart-world-map'; +import WorldmapChartPlugin from '@superset-ui/plugin-chart-world-map'; new WorldmapChartPlugin().configure({ key: 'world-map' }).register(); ``` diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json b/superset-frontend/plugins/plugin-chart-world-map/package.json similarity index 82% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/package.json rename to superset-frontend/plugins/plugin-chart-world-map/package.json index cf9ed4f39fed..17bf653603d5 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/package.json +++ b/superset-frontend/plugins/plugin-chart-world-map/package.json @@ -1,5 +1,5 @@ { - "name": "@superset-ui/legacy-plugin-chart-world-map", + "name": "@superset-ui/plugin-chart-world-map", "version": "0.20.3", "description": "Superset Legacy Chart - World Map", "sideEffects": [ @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/legacy-plugin-chart-world-map" + "directory": "superset-frontend/plugins/plugin-chart-world-map" }, "keywords": [ "superset" @@ -24,7 +24,7 @@ "bugs": { "url": "https://github.com/apache/superset/issues" }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-plugin-chart-world-map#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/plugin-chart-world-map#readme", "publishConfig": { "access": "public" }, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.tsx b/superset-frontend/plugins/plugin-chart-world-map/src/ReactWorldMap.tsx similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.tsx rename to superset-frontend/plugins/plugin-chart-world-map/src/ReactWorldMap.tsx diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts b/superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/WorldMap.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts b/superset-frontend/plugins/plugin-chart-world-map/src/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/buildQuery.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts b/superset-frontend/plugins/plugin-chart-world-map/src/countries.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/countries.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/countries.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap1-dark.jpg b/superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap1-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap1-dark.jpg rename to superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap1-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap1.jpg b/superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap1.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap1.jpg rename to superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap1.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap2-dark.jpg b/superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap2-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap2-dark.jpg rename to superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap2-dark.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap2.jpg b/superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap2.jpg similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/WorldMap2.jpg rename to superset-frontend/plugins/plugin-chart-world-map/src/images/WorldMap2.jpg diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnail-dark.png rename to superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnail.png rename to superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnailLarge.png b/superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/images/thumbnailLarge.png rename to superset-frontend/plugins/plugin-chart-world-map/src/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts b/superset-frontend/plugins/plugin-chart-world-map/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/stories/WorldMap.stories.tsx b/superset-frontend/plugins/plugin-chart-world-map/src/stories/WorldMap.stories.tsx similarity index 92% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/stories/WorldMap.stories.tsx rename to superset-frontend/plugins/plugin-chart-world-map/src/stories/WorldMap.stories.tsx index 1987ade1a051..8bf461e207fc 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/stories/WorldMap.stories.tsx +++ b/superset-frontend/plugins/plugin-chart-world-map/src/stories/WorldMap.stories.tsx @@ -19,14 +19,14 @@ /* eslint-disable no-magic-numbers, sort-keys */ import { SuperChart } from '@superset-ui/core'; -import WorldMapChartPlugin from '@superset-ui/legacy-plugin-chart-world-map'; +import WorldMapChartPlugin from '@superset-ui/plugin-chart-world-map'; import { withResizableChartDemo } from '@storybook-shared'; import data from './data'; new WorldMapChartPlugin().configure({ key: 'world-map' }).register(); export default { - title: 'Legacy Chart Plugins/legacy-plugin-chart-world-map', + title: 'Chart Plugins/plugin-chart-world-map', decorators: [withResizableChartDemo], args: { maxBubbleSize: 25, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/stories/data.ts b/superset-frontend/plugins/plugin-chart-world-map/src/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/stories/data.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/stories/data.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts b/superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformData.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/transformData.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts b/superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/utils.ts b/superset-frontend/plugins/plugin-chart-world-map/src/utils.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/utils.ts rename to superset-frontend/plugins/plugin-chart-world-map/src/utils.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts rename to superset-frontend/plugins/plugin-chart-world-map/test/WorldMap.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/buildQuery.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/test/buildQuery.test.ts rename to superset-frontend/plugins/plugin-chart-world-map/test/buildQuery.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformData.test.ts rename to superset-frontend/plugins/plugin-chart-world-map/test/transformData.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/test/transformProps.test.ts rename to superset-frontend/plugins/plugin-chart-world-map/test/transformProps.test.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/tsconfig.json b/superset-frontend/plugins/plugin-chart-world-map/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/test/tsconfig.json rename to superset-frontend/plugins/plugin-chart-world-map/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json b/superset-frontend/plugins/plugin-chart-world-map/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/tsconfig.json rename to superset-frontend/plugins/plugin-chart-world-map/tsconfig.json diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts b/superset-frontend/plugins/plugin-chart-world-map/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts rename to superset-frontend/plugins/plugin-chart-world-map/types/external.d.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/CHANGELOG.md b/superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/CHANGELOG.md rename to superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/README.md b/superset-frontend/plugins/preset-chart-nvd3/README.md similarity index 78% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/README.md rename to superset-frontend/plugins/preset-chart-nvd3/README.md index b2cc76eff06f..f09ba2661224 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/README.md +++ b/superset-frontend/plugins/preset-chart-nvd3/README.md @@ -17,10 +17,10 @@ specific language governing permissions and limitations under the License. --> -## @superset-ui/legacy-preset-chart-nvd3 +## @superset-ui/preset-chart-nvd3 -[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-nvd3.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/legacy-preset-chart-nvd3) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Flegacy-preset-chart-nvd3?style=flat)](https://libraries.io/npm/@superset-ui%2Flegacy-preset-chart-nvd3) +[![Version](https://img.shields.io/npm/v/@superset-ui/preset-chart-nvd3.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/preset-chart-nvd3) +[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fpreset-chart-nvd3?style=flat)](https://libraries.io/npm/@superset-ui%2Fpreset-chart-nvd3) This plugin provides Big Number for Superset. @@ -29,7 +29,7 @@ This plugin provides Big Number for Superset. Import the preset and register. This will register all the chart plugins under nvd3. ```js -import { NVD3ChartPreset } from '@superset-ui/legacy-preset-chart-nvd3'; +import { NVD3ChartPreset } from '@superset-ui/preset-chart-nvd3'; new NVD3ChartPreset().register(); ``` @@ -41,7 +41,7 @@ This `key` will be used to lookup this chart throughout the app. import { AreaChartPlugin, LineChartPlugin, -} from '@superset-ui/legacy-preset-chart-nvd3'; +} from '@superset-ui/preset-chart-nvd3'; new AreaChartPlugin().configure({ key: 'area' }).register(); new LineChartPlugin().configure({ key: 'line' }).register(); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json b/superset-frontend/plugins/preset-chart-nvd3/package.json similarity index 85% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/package.json rename to superset-frontend/plugins/preset-chart-nvd3/package.json index 1f1f1890d211..2c39a92f9b79 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json +++ b/superset-frontend/plugins/preset-chart-nvd3/package.json @@ -1,5 +1,5 @@ { - "name": "@superset-ui/legacy-preset-chart-nvd3", + "name": "@superset-ui/preset-chart-nvd3", "version": "0.20.3", "description": "Superset Legacy Chart - NVD3", "sideEffects": [ @@ -14,7 +14,7 @@ "repository": { "type": "git", "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/packages/legacy-preset-chart-nvd3" + "directory": "superset-frontend/plugins/preset-chart-nvd3" }, "keywords": [ "superset" @@ -24,7 +24,7 @@ "bugs": { "url": "https://github.com/apache/superset/issues" }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/legacy-preset-chart-nvd3#readme", + "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/preset-chart-nvd3#readme", "publishConfig": { "access": "public" }, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/buildQuery.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/controlPanel.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/controlPanel.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/example-dark.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/example-dark.jpg rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example-dark.jpg diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/example.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example.jpg similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/example.jpg rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example.jpg diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnail.png b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnail.png rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnailLarge.png b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/images/thumbnailLarge.png rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx similarity index 94% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx index 5cdb4a40f5e7..acf724a9e18e 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx +++ b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx @@ -18,14 +18,14 @@ */ import { SuperChart, VizType } from '@superset-ui/core'; -import { BulletChartPlugin } from '@superset-ui/legacy-preset-chart-nvd3'; +import { BulletChartPlugin } from '@superset-ui/preset-chart-nvd3'; import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; import data from './data'; new BulletChartPlugin().configure({ key: VizType.Bullet }).register(); export default { - title: 'Legacy Chart Plugins/legacy-preset-chart-nvd3/Bullet', + title: 'Chart Plugins/preset-chart-nvd3/Bullet', decorators: [withResizableChartDemo], args: { ranges: '0, 50, 75, 100', diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/stories/data.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/stories/data.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Controls.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Controls.tsx rename to superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.ts b/superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.tsx rename to superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/buildQuery.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/controlPanel.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/controlPanel.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/example-dark.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example-dark.jpg similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/example-dark.jpg rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example-dark.jpg diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/example.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example.jpg similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/example.jpg rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example.jpg diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnail-dark.png b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail-dark.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnail-dark.png rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail-dark.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnail.png b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnail.png rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnailLarge.png b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnailLarge.png similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/images/thumbnailLarge.png rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnailLarge.png diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformData.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/transformProps.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts b/superset-frontend/plugins/preset-chart-nvd3/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts b/superset-frontend/plugins/preset-chart-nvd3/src/preset.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/preset.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts b/superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/utils.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/tokenize.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/tokenize.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts b/superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts rename to superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/Bullet.test.ts rename to superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot.test.ts rename to superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/tsconfig.json b/superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/tsconfig.json rename to superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.ts rename to superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/utils/isTruthy.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.ts rename to superset-frontend/plugins/preset-chart-nvd3/test/utils/isTruthy.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/utils/tokenize.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.ts rename to superset-frontend/plugins/preset-chart-nvd3/test/utils/tokenize.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json b/superset-frontend/plugins/preset-chart-nvd3/tsconfig.json similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/tsconfig.json rename to superset-frontend/plugins/preset-chart-nvd3/tsconfig.json diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts b/superset-frontend/plugins/preset-chart-nvd3/types/external.d.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts rename to superset-frontend/plugins/preset-chart-nvd3/types/external.d.ts diff --git a/superset-frontend/scripts/check-custom-rules.js b/superset-frontend/scripts/check-custom-rules.js index 58c82787ffda..8bde4a7793ca 100755 --- a/superset-frontend/scripts/check-custom-rules.js +++ b/superset-frontend/scripts/check-custom-rules.js @@ -685,7 +685,10 @@ function main() { /\/esm\//, /\/lib\//, /\/dist\//, - /plugins\/legacy-/, // Legacy plugins can have old color patterns + // formerly the legacy-* plugins; they keep old color patterns and + // eager t() calls pending modernization + /plugins\/plugin-chart-(calendar|chord|country-map|horizon|paired-t-test|parallel-coordinates|partition|rose|world-map)\//, + /plugins\/preset-chart-nvd3\//, /plugin-chart-point-cluster-map\/src\/controlPanel/, // Data visualization color choices /\/vendor\//, // Third-party vendor code /spec\/fixtures\//, // Test fixtures diff --git a/superset-frontend/scripts/check-storybook-coverage.js b/superset-frontend/scripts/check-storybook-coverage.js index 7c87f8b244b6..9de6f5b74c8e 100644 --- a/superset-frontend/scripts/check-storybook-coverage.js +++ b/superset-frontend/scripts/check-storybook-coverage.js @@ -30,8 +30,8 @@ const ROOT = path.resolve(__dirname, '..'); // Directories to check for storybook coverage const PLUGIN_PATTERNS = [ 'plugins/plugin-chart-*/src', - 'plugins/legacy-plugin-chart-*/src', - 'plugins/legacy-preset-chart-*/src', + 'plugins/plugin-chart-*/src', + 'plugins/preset-chart-*/src', ]; const CORE_COMPONENT_PATTERNS = ['packages/superset-ui-core/src/components/*']; diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index c7e3e9f6e34c..5aa9c9b8576a 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -22,21 +22,21 @@ import { Preset, VizType, } from '@superset-ui/core'; -import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar'; -import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord'; -import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map'; -import HorizonChartPlugin from '@superset-ui/legacy-plugin-chart-horizon'; -import PairedTTestChartPlugin from '@superset-ui/legacy-plugin-chart-paired-t-test'; -import ParallelCoordinatesChartPlugin from '@superset-ui/legacy-plugin-chart-parallel-coordinates'; -import PartitionChartPlugin from '@superset-ui/legacy-plugin-chart-partition'; -import RoseChartPlugin from '@superset-ui/legacy-plugin-chart-rose'; +import CalendarChartPlugin from '@superset-ui/plugin-chart-calendar'; +import ChordChartPlugin from '@superset-ui/plugin-chart-chord'; +import CountryMapChartPlugin from '@superset-ui/plugin-chart-country-map'; +import HorizonChartPlugin from '@superset-ui/plugin-chart-horizon'; +import PairedTTestChartPlugin from '@superset-ui/plugin-chart-paired-t-test'; +import ParallelCoordinatesChartPlugin from '@superset-ui/plugin-chart-parallel-coordinates'; +import PartitionChartPlugin from '@superset-ui/plugin-chart-partition'; +import RoseChartPlugin from '@superset-ui/plugin-chart-rose'; import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; -import WorldMapChartPlugin from '@superset-ui/legacy-plugin-chart-world-map'; +import WorldMapChartPlugin from '@superset-ui/plugin-chart-world-map'; import { BulletChartPlugin, TimePivotChartPlugin, -} from '@superset-ui/legacy-preset-chart-nvd3'; +} from '@superset-ui/preset-chart-nvd3'; import { DeckGLChartPreset } from '@superset-ui/preset-chart-deckgl'; import ScatterMapChartPlugin from '@superset-ui/plugin-chart-point-cluster-map'; import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; diff --git a/superset-frontend/tsconfig.json b/superset-frontend/tsconfig.json index 6a05ebca0d60..8e1f33234342 100644 --- a/superset-frontend/tsconfig.json +++ b/superset-frontend/tsconfig.json @@ -58,8 +58,8 @@ "@apache-superset/core": ["./packages/superset-core/src"], "@apache-superset/core/*": ["./packages/superset-core/src/*"], "@superset-ui/plugin-chart-*": ["./plugins/plugin-chart-*/src"], - "@superset-ui/legacy-plugin-chart-*": ["./plugins/legacy-plugin-chart-*/src"], - "@superset-ui/legacy-preset-chart-*": ["./plugins/legacy-preset-chart-*/src"], + "@superset-ui/plugin-chart-*": ["./plugins/plugin-chart-*/src"], + "@superset-ui/preset-chart-*": ["./plugins/preset-chart-*/src"], "echarts/types/src/*": ["./node_modules/echarts/types/src/*"] } }, @@ -79,16 +79,16 @@ { "path": "./packages/superset-ui-core" }, { "path": "./packages/superset-ui-chart-controls" }, { "path": "./packages/superset-ui-switchboard" }, - { "path": "./plugins/legacy-plugin-chart-calendar" }, - { "path": "./plugins/legacy-plugin-chart-chord" }, - { "path": "./plugins/legacy-plugin-chart-country-map" }, - { "path": "./plugins/legacy-plugin-chart-horizon" }, - { "path": "./plugins/legacy-plugin-chart-paired-t-test" }, - { "path": "./plugins/legacy-plugin-chart-parallel-coordinates" }, - { "path": "./plugins/legacy-plugin-chart-partition" }, - { "path": "./plugins/legacy-plugin-chart-rose" }, - { "path": "./plugins/legacy-plugin-chart-world-map" }, - { "path": "./plugins/legacy-preset-chart-nvd3" }, + { "path": "./plugins/plugin-chart-calendar" }, + { "path": "./plugins/plugin-chart-chord" }, + { "path": "./plugins/plugin-chart-country-map" }, + { "path": "./plugins/plugin-chart-horizon" }, + { "path": "./plugins/plugin-chart-paired-t-test" }, + { "path": "./plugins/plugin-chart-parallel-coordinates" }, + { "path": "./plugins/plugin-chart-partition" }, + { "path": "./plugins/plugin-chart-rose" }, + { "path": "./plugins/plugin-chart-world-map" }, + { "path": "./plugins/preset-chart-nvd3" }, { "path": "./plugins/plugin-chart-ag-grid-table" }, { "path": "./plugins/plugin-chart-cartodiagram" }, { "path": "./plugins/preset-chart-deckgl" }, diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index e9043ab2004b..020391c0f3aa 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -105,7 +105,7 @@ if (!isDevMode) { const plugins = [ new webpack.ProvidePlugin({ process: 'process/browser.js', - ...(isDevMode ? { Buffer: ['buffer', 'Buffer'] } : {}), // Fix legacy-plugin-chart-paired-t-test broken Story + ...(isDevMode ? { Buffer: ['buffer', 'Buffer'] } : {}), // Fix plugin-chart-paired-t-test broken Story }), // creates a manifest.json mapping of name to hashed output used in template files @@ -479,7 +479,7 @@ const config = { vm: false, path: false, stream: require.resolve('stream-browserify'), - ...(isDevMode ? { buffer: require.resolve('buffer/') } : {}), // Fix legacy-plugin-chart-paired-t-test broken Story + ...(isDevMode ? { buffer: require.resolve('buffer/') } : {}), // Fix plugin-chart-paired-t-test broken Story }, }, context: APP_DIR, // to automatically find tsconfig.json From b3435a04cf4407a3d2c09c4651ed8091d277d856 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 3 Jul 2026 10:20:14 -0700 Subject: [PATCH 24/84] chore(viz)!: remove explore_json endpoints, viz.py and the legacy chart data pipeline (#41750) Co-authored-by: Claude Code --- .../src/chart/clients/ChartClient.ts | 23 +- .../chart/components/StatefulChart.test.tsx | 52 +- .../src/chart/components/StatefulChart.tsx | 41 +- .../src/chart/models/ChartMetadata.ts | 5 - .../src/chart/models/ChartProps.ts | 1 - .../test/chart/clients/ChartClient.test.ts | 36 - .../preset-chart-deckgl/src/utils/explore.ts | 68 - .../components/Chart/ChartRenderer.test.tsx | 1 - .../src/components/Chart/ChartRenderer.tsx | 1 - .../components/Chart/DrillBy/DrillByModal.tsx | 13 +- .../Chart/DrillBy/DrillBySubmenu.test.tsx | 1 - .../src/components/Chart/chartAction.ts | 90 +- .../src/components/Chart/chartActions.test.ts | 222 +- .../ExploreChartPanel.test.tsx | 1 - .../components/ExploreChartPanel/index.tsx | 23 - .../ExploreViewContainer.test.tsx | 2 - .../exploreUtils/exploreUtils.test.tsx | 57 +- .../explore/exploreUtils/exportChart.test.ts | 133 - .../exploreUtils/getExploreUrl.test.ts | 28 +- .../getLegacyEndpointType.test.ts | 40 - .../exploreUtils/getURIDirectory.test.ts | 14 +- .../src/explore/exploreUtils/index.ts | 77 +- .../exploreUtils/shouldUseLegacyApi.test.ts | 59 - superset/async_events/async_query_manager.py | 33 +- superset/charts/api.py | 2 +- superset/commands/chart/warm_up_cache.py | 33 +- superset/common/query_context_processor.py | 25 - superset/config.py | 1 - superset/datasets/api.py | 2 +- superset/middleware/legacy_prefix_redirect.py | 8 +- superset/models/slice.py | 18 +- superset/security/manager.py | 9 +- superset/tasks/async_queries.py | 81 +- superset/views/core.py | 258 +- superset/views/utils.py | 103 +- superset/viz.py | 2732 ----------------- tests/integration_tests/core_tests.py | 103 +- .../security/guest_token_security_tests.py | 28 +- tests/integration_tests/security_tests.py | 27 +- .../tasks/async_queries_tests.py | 118 - tests/integration_tests/utils_tests.py | 33 +- tests/integration_tests/viz_tests.py | 2456 --------------- .../async_events/async_query_manager_tests.py | 54 - .../commands/chart/warm_up_cache_test.py | 342 +-- .../middleware/test_legacy_prefix_redirect.py | 8 - tests/unit_tests/security/manager_test.py | 3 - tests/unit_tests/tasks/test_async_queries.py | 169 - tests/unit_tests/test_subdirectory_url_for.py | 13 - tests/unit_tests/test_viz_cache_key.py | 111 - tests/unit_tests/test_viz_get_df_payload.py | 244 -- 50 files changed, 188 insertions(+), 7814 deletions(-) delete mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts delete mode 100644 superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts delete mode 100644 superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts delete mode 100644 superset/viz.py delete mode 100644 tests/integration_tests/viz_tests.py delete mode 100644 tests/unit_tests/test_viz_cache_key.py delete mode 100644 tests/unit_tests/test_viz_get_df_payload.py diff --git a/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts b/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts index 397a854a2a5d..52c8d76c6e44 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/clients/ChartClient.ts @@ -104,24 +104,15 @@ export default class ChartClient { const buildQueryRegistry = getChartBuildQueryRegistry(); if (metaDataRegistry.has(visType)) { - const { useLegacyApi } = metaDataRegistry.get(visType)!; const buildQuery = (await buildQueryRegistry.get(visType)) ?? (() => formData); - const requestConfig: RequestConfig = useLegacyApi - ? { - endpoint: '/explore_json/', - postPayload: { - form_data: buildQuery(formData), - }, - ...options, - } - : { - endpoint: '/api/v1/chart/data', - jsonPayload: { - query_context: buildQuery(formData), - }, - ...options, - }; + const requestConfig: RequestConfig = { + endpoint: '/api/v1/chart/data', + jsonPayload: { + query_context: buildQuery(formData), + }, + ...options, + }; return this.client .post(requestConfig) diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx index e1109a337100..f66ad3a516fd 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.test.tsx @@ -21,7 +21,6 @@ import { render, waitFor, configure, act } from '@testing-library/react'; import '@testing-library/jest-dom'; import StatefulChart from './StatefulChart'; import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton'; -import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton'; import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton'; // Configure testing library to use data-test attribute @@ -29,7 +28,6 @@ configure({ testIdAttribute: 'data-test' }); // Mock the registries jest.mock('../registries/ChartControlPanelRegistrySingleton'); -jest.mock('../registries/ChartMetadataRegistrySingleton'); jest.mock('../registries/ChartBuildQueryRegistrySingleton'); jest.mock('../clients/ChartClient'); @@ -67,12 +65,6 @@ beforeEach(() => { jest.clearAllMocks(); // Setup default registry mocks - jest.mocked(getChartMetadataRegistry).mockReturnValue({ - get: jest.fn().mockReturnValue({ - useLegacyApi: false, - }), - } as unknown as ReturnType); - jest.mocked(getChartBuildQueryRegistry).mockReturnValue({ get: jest.fn().mockResolvedValue(null), } as unknown as ReturnType); @@ -746,11 +738,10 @@ test('resolves async (202) responses via the injected handleAsyncChartData hook' await waitFor(() => { expect(handleAsyncChartData).toHaveBeenCalledTimes(1); }); - // Delegates the raw response + job metadata (and useLegacyApi + abort signal) + // Delegates the raw response + job metadata (and abort signal) expect(handleAsyncChartData).toHaveBeenCalledWith( { status: 202 }, asyncJob, - false, expect.any(AbortSignal), ); // Chart renders once the async data resolves @@ -798,43 +789,6 @@ test('renders synchronous (200) responses that include a response object', async expect(mockChartClient.client.post).toHaveBeenCalledTimes(1); }); -test('wraps the legacy async body as { result: [body] } for the async handler', async () => { - const legacyBody = { job_id: 'j1', channel_id: 'c1', status: 'running' }; - mockChartClient.client.post.mockResolvedValue({ - response: { status: 202 } as Response, - json: legacyBody, - }); - // Force the legacy API path for this viz type - jest.mocked(getChartMetadataRegistry).mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true }), - } as unknown as ReturnType); - const handleAsyncChartData = jest - .fn() - .mockResolvedValue([{ data: 'legacy result' }]); - - const { getByTestId } = render( - , - ); - - await waitFor(() => { - expect(handleAsyncChartData).toHaveBeenCalledTimes(1); - }); - // Legacy body must be wrapped to match the V1 response signature - expect(handleAsyncChartData).toHaveBeenCalledWith( - { status: 202 }, - { result: [legacyBody] }, - true, - expect.any(AbortSignal), - ); - await waitFor(() => { - expect(getByTestId('super-chart')).toBeInTheDocument(); - }); -}); - test('does not apply a superseded async response over a newer one', async () => { mockChartClient.client.post.mockResolvedValue({ response: { status: 202 } as Response, @@ -1024,7 +978,7 @@ test('passes an abort signal to the async handler and aborts it on unmount', asy response: { status: 202 } as Response, json: { job_id: 'j', channel_id: 'c' }, }); - // Typed with a rest param so mock.calls is indexable (the 4th arg is the signal) + // Typed with a rest param so mock.calls is indexable (the 3rd arg is the signal) const handleAsyncChartData = jest.fn( (..._args: unknown[]) => new Promise(() => {}), // never resolves ); @@ -1040,7 +994,7 @@ test('passes an abort signal to the async handler and aborts it on unmount', asy await waitFor(() => { expect(handleAsyncChartData).toHaveBeenCalledTimes(1); }); - const signal = handleAsyncChartData.mock.calls[0][3] as AbortSignal; + const signal = handleAsyncChartData.mock.calls[0][2] as AbortSignal; expect(signal).toBeInstanceOf(AbortSignal); expect(signal.aborted).toBe(false); diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx index 05aa3517f3d1..fbdf8d977c96 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/StatefulChart.tsx @@ -34,7 +34,6 @@ import { import { Loading } from '../../components/Loading'; import ChartClient from '../clients/ChartClient'; import getChartBuildQueryRegistry from '../registries/ChartBuildQueryRegistrySingleton'; -import getChartMetadataRegistry from '../registries/ChartMetadataRegistrySingleton'; import getChartControlPanelRegistry from '../registries/ChartControlPanelRegistrySingleton'; import SuperChart from './SuperChart'; @@ -281,9 +280,6 @@ export default function StatefulChart(props: StatefulChartProps) { } finalFormData.viz_type = vizType; - // Get chart metadata - const { useLegacyApi } = getChartMetadataRegistry().get(vizType) || {}; - // Build query using the chart's buildQuery function const buildQuery = await getChartBuildQueryRegistry().get(vizType); let queryContext; @@ -295,31 +291,20 @@ export default function StatefulChart(props: StatefulChartProps) { queryContext = buildQueryContext(finalFormData); } - // Ensure query_context is properly formatted for new API - if (!useLegacyApi && !queryContext.queries) { + // Ensure query_context is properly formatted for the API + if (!queryContext.queries) { queryContext = { queries: [queryContext] }; } - const endpoint = useLegacyApi ? '/explore_json/' : '/api/v1/chart/data'; const requestConfig: RequestConfig = { - endpoint, + endpoint: '/api/v1/chart/data', signal: controller.signal, ...(timeout && { timeout: timeout * 1000 }), - }; - - if (useLegacyApi) { - requestConfig.postPayload = { - form_data: { - ...finalFormData, - ...(force && { force: true }), - }, - }; - } else { - requestConfig.jsonPayload = { + jsonPayload: { ...queryContext, ...(force && { force: true }), - }; - } + }, + }; const clientResponse = await chartClientRef.current!.client.post(requestConfig); @@ -347,18 +332,10 @@ export default function StatefulChart(props: StatefulChartProps) { 'the async handler or disable GLOBAL_ASYNC_QUERIES for this chart.', ); } - // The async handler (handleChartDataResponse) expects the V1 chart data - // response signature. The legacy endpoint returns a flat body, so wrap - // it as { result: [body] } exactly like legacyChartDataRequest does for - // the standard chart path; the V1 body is already correctly shaped. - const asyncPayload = useLegacyApi - ? ({ result: [clientResponse.json] } as JsonObject) - : (clientResponse.json as JsonObject); responseData = ensureIsArray( await hooks.handleAsyncChartData( rawResponse, - asyncPayload, - useLegacyApi, + clientResponse.json as JsonObject, controller.signal, ), ); @@ -374,9 +351,9 @@ export default function StatefulChart(props: StatefulChartProps) { : [clientResponse.json] ) as JsonObject[]; - // Handle the nested result structure from the new API + // Handle the nested result structure from the API responseData = ( - !useLegacyApi && rows[0]?.result ? rows[0].result : rows + rows[0]?.result ? rows[0].result : rows ) as QueryData[]; } diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts index 3b5073f02b37..f8bd22d4fb2a 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartMetadata.ts @@ -40,7 +40,6 @@ export interface ChartMetadataConfig { supportedAnnotationTypes?: string[]; thumbnail: string; thumbnailDark?: string; - useLegacyApi?: boolean; behaviors?: Behavior[]; exampleGallery?: ExampleImage[]; tags?: string[]; @@ -75,8 +74,6 @@ export default class ChartMetadata { thumbnailDark?: string; - useLegacyApi: boolean; - behaviors: Behavior[]; datasourceCount: number; @@ -112,7 +109,6 @@ export default class ChartMetadata { supportedAnnotationTypes = [], thumbnail, thumbnailDark, - useLegacyApi = false, behaviors = [], datasourceCount = 1, enableNoResults = true, @@ -144,7 +140,6 @@ export default class ChartMetadata { this.supportedAnnotationTypes = supportedAnnotationTypes; this.thumbnail = thumbnail; this.thumbnailDark = thumbnailDark; - this.useLegacyApi = useLegacyApi; this.behaviors = behaviors; this.datasourceCount = datasourceCount; this.enableNoResults = enableNoResults; diff --git a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts index c372a0cc691b..638b5888a901 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts +++ b/superset-frontend/packages/superset-ui-core/src/chart/models/ChartProps.ts @@ -75,7 +75,6 @@ type Hooks = { handleAsyncChartData?: ( response: Response, json: JsonObject, - useLegacyApi?: boolean, signal?: AbortSignal, ) => Promise | QueryData[]; } & PlainObject; diff --git a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts index 8d11b98d1754..6ec2ef0f4874 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/chart/clients/ChartClient.test.ts @@ -160,42 +160,6 @@ describe('ChartClient', () => { datasource: '1__table', }), ).rejects.toEqual(new Error('Unknown chart type: rainbow_3d_pie'))); - - test('fetches data from the legacy API if ChartMetadata has useLegacyApi=true,', () => { - // note legacy charts do not register a buildQuery function in the registry - getChartMetadataRegistry().registerValue( - 'word_cloud_legacy', - new ChartMetadata({ - name: 'Legacy Word Cloud', - thumbnail: '.png', - useLegacyApi: true, - }), - ); - - fetchMock.post('glob:*/api/v1/chart/data', () => - Promise.reject(new Error('Unexpected all to v1 API')), - ); - - // post `Superset.route_base = ""`, the legacy endpoint - // collapsed from `/superset/explore_json/` to `/explore_json/`. - fetchMock.post('glob:*/explore_json/', { - field1: 'abc', - field2: 'def', - }); - - return expect( - chartClient.loadQueryData({ - granularity: 'minute', - viz_type: 'word_cloud_legacy', - datasource: '1__table', - }), - ).resolves.toEqual([ - { - field1: 'abc', - field2: 'def', - }, - ]); - }); }); describe('.loadDatasource(datasourceKey, options)', () => { diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts b/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts deleted file mode 100644 index 0d5d6e251c5c..000000000000 --- a/superset-frontend/plugins/preset-chart-deckgl/src/utils/explore.ts +++ /dev/null @@ -1,68 +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 URI from 'urijs'; -import { JsonObject } from '@superset-ui/core'; -import { safeStringify } from './safeStringify'; - -const MAX_URL_LENGTH = 8000; - -export function getURIDirectory(endpointType = 'base') { - // Building the directory part of the URI - let directory = '/explore/'; - if (['json', 'csv', 'query', 'results', 'samples'].includes(endpointType)) { - directory = '/explore_json/'; - } - - return directory; -} - -export function getExploreLongUrl( - formData: JsonObject, - endpointType: string, - allowOverflow = true, - extraSearch: Record = {}, -): string | undefined { - if (!formData.datasource) { - return undefined; - } - - const uri = new URI('/'); - const directory = getURIDirectory(endpointType); - const search = uri.search(true); - Object.keys(extraSearch).forEach(key => { - search[key] = extraSearch[key]; - }); - search.form_data = safeStringify(formData); - if (endpointType === 'standalone') { - search.standalone = 'true'; - } - const url = uri.directory(directory).search(search).toString(); - if (!allowOverflow && url.length > MAX_URL_LENGTH) { - const minimalFormData = { - datasource: formData.datasource, - viz_type: formData.viz_type, - }; - - return getExploreLongUrl(minimalFormData, endpointType, false, { - URL_IS_TOO_LONG_TO_SHARE: null, - }); - } - - return url; -} diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx index 92a2b8c52db9..672a3995c7b6 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx @@ -138,7 +138,6 @@ test('should not render chart context menu if the context menu is suppressed for new ChartMetadata({ name: 'chart with suppressed context menu', thumbnail: '.png', - useLegacyApi: false, suppressContextMenu: true, }), ); diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx index b192c9cf1f2c..3c6ebab63d8e 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx @@ -169,7 +169,6 @@ interface ChartHooks { handleAsyncChartData?: ( response: Response, json: JsonObject, - useLegacyApi?: boolean, signal?: AbortSignal, ) => Promise | QueryData[]; } diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx index 83736a8cf947..fc490a2d257b 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.tsx @@ -54,7 +54,7 @@ import { LOG_ACTIONS_FURTHER_DRILL_BY, } from 'src/logger/LogUtils'; import { findPermission } from 'src/utils/findPermission'; -import { getQuerySettings, exportChart } from 'src/explore/exploreUtils'; +import { exportChart } from 'src/explore/exploreUtils'; import { isEmbedded } from 'src/dashboard/util/isEmbedded'; import { Dataset, DrillByType } from '../types'; import DrillByChart from './DrillByChart'; @@ -401,13 +401,11 @@ export default function DrillByModal({ const handleReload = useCallback(() => { setChartDataResult(undefined); setIsChartDataLoading(true); - const [useLegacyApi] = getQuerySettings(drilledFormData); + getChartDataRequest({ formData: drilledFormData, }) - .then(({ response, json }) => - handleChartDataResponse(response, json, useLegacyApi), - ) + .then(({ response, json }) => handleChartDataResponse(response, json)) .then(queriesResponse => { setChartDataResult(queriesResponse); }) @@ -491,15 +489,12 @@ export default function DrillByModal({ useEffect(() => { if (drilledFormData) { - const [useLegacyApi] = getQuerySettings(drilledFormData); setIsChartDataLoading(true); setChartDataResult(undefined); getChartDataRequest({ formData: drilledFormData, }) - .then(({ response, json }) => - handleChartDataResponse(response, json, useLegacyApi), - ) + .then(({ response, json }) => handleChartDataResponse(response, json)) .then(queriesResponse => { setChartDataResult(queriesResponse); }) diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx index c9d3d5a473fb..029ab4501304 100644 --- a/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx +++ b/superset-frontend/src/components/Chart/DrillBy/DrillBySubmenu.test.tsx @@ -129,7 +129,6 @@ getChartMetadataRegistry().registerValue( new ChartMetadata({ name: 'fake pie', thumbnail: '.png', - useLegacyApi: false, behaviors: [Behavior.DrillBy], }), ); diff --git a/superset-frontend/src/components/Chart/chartAction.ts b/superset-frontend/src/components/Chart/chartAction.ts index 2e6854e07910..c80cb7aad314 100644 --- a/superset-frontend/src/components/Chart/chartAction.ts +++ b/superset-frontend/src/components/Chart/chartAction.ts @@ -36,8 +36,6 @@ import type { ControlStateMapping } from '@superset-ui/chart-controls'; import { getControlsState } from 'src/explore/store'; import { getAnnotationJsonUrl, - getExploreUrl, - getLegacyEndpointType, buildV1ChartDataPayload, getQuerySettings, getChartDataUri, @@ -406,51 +404,6 @@ export const dynamicPluginControlsReady = }); }; -const legacyChartDataRequest = async ( - formData: QueryFormData | LatestQueryFormData, - resultFormat: string, - resultType: string, - force: boolean, - method: 'GET' | 'POST' = 'POST', - requestParams: RequestParams = {}, - parseMethod?: string, -): Promise => { - const endpointType = getLegacyEndpointType({ resultFormat, resultType }); - const allowDomainSharding = Boolean( - // eslint-disable-next-line camelcase - domainShardingEnabled && requestParams?.dashboard_id, - ); - const url = getExploreUrl({ - formData: formData as QueryFormData & { - label_colors?: Record; - }, - endpointType, - force, - allowDomainSharding, - method, - requestParams: requestParams.dashboard_id - ? { dashboard_id: String(requestParams.dashboard_id) } - : {}, - }); - const querySettings: QuerySettings = { - ...requestParams, - url: url ?? undefined, - postPayload: { form_data: formData }, - parseMethod, - }; - - return SupersetClient.post( - querySettings as Parameters[0], - ).then(({ json, response }: { json: JsonObject; response: Response }) => - // Make the legacy endpoint return a payload that corresponds to the - // V1 chart data endpoint response signature. - ({ - response, - json: { result: [json] }, - }), - ); -}; - const v1ChartDataRequest = async ( formData: QueryFormData | LatestQueryFormData, resultFormat: string, @@ -523,18 +476,7 @@ export async function getChartDataRequest({ credentials: 'include', }; } - const [useLegacyApi, parseMethod] = getQuerySettings(formData); - if (useLegacyApi) { - return legacyChartDataRequest( - formData, - resultFormat, - resultType, - force, - method, - querySettings, - parseMethod, - ); - } + const [parseMethod] = getQuerySettings(formData); return v1ChartDataRequest( formData, resultFormat, @@ -699,7 +641,6 @@ export function addChart( export function handleChartDataResponse( response: Response, json: { result: QueryData[] }, - useLegacyApi?: boolean, signal?: AbortSignal, ): Promise | QueryData[] { if (isFeatureEnabled(FeatureFlag.GlobalAsyncQueries)) { @@ -715,12 +656,6 @@ export function handleChartDataResponse( // which differs from QueryData. We cast through unknown to handle this safely. // The optional signal lets a caller (e.g. StatefulChart) cancel the wait // when its chart is superseded or unmounted, avoiding leaked listeners. - if (useLegacyApi) { - return waitForAsyncData( - result[0] as unknown as Parameters[0], - signal, - ) as Promise; - } return waitForAsyncData( result as unknown as Parameters[0], signal, @@ -783,11 +718,8 @@ export function exploreJSON( ownState, }); - const [useLegacyApi] = getQuerySettings(formData); const chartDataRequestCaught = chartDataRequest - .then(({ response, json }) => - handleChartDataResponse(response, json, useLegacyApi), - ) + .then(({ response, json }) => handleChartDataResponse(response, json)) .then(queriesResponse => { // Drop stale responses: if a newer query has started for this chart, // its controller will have replaced ours in state, so ignore this @@ -932,28 +864,10 @@ export function exploreJSON( }, ); - // only retrieve annotations when calling the legacy API - const annotationLayers: AnnotationLayer[] = useLegacyApi - ? (formData.annotation_layers as AnnotationLayer[]) || [] - : []; - const isDashboardRequest = (dashboardId ?? 0) > 0; - return Promise.all([ chartDataRequestCaught, dispatch(triggerQuery(false, key as string | number)), dispatch(updateQueryFormData(formData, key as string | number)), - ...annotationLayers.map((annotation: AnnotationLayer) => - dispatch( - runAnnotationQuery({ - annotation, - timeout, - formData, - key, - isDashboardRequest, - force, - }), - ), - ), ]); }; } diff --git a/superset-frontend/src/components/Chart/chartActions.test.ts b/superset-frontend/src/components/Chart/chartActions.test.ts index 7bfe8dcc5803..c1d0781d0565 100644 --- a/superset-frontend/src/components/Chart/chartActions.test.ts +++ b/superset-frontend/src/components/Chart/chartActions.test.ts @@ -32,7 +32,6 @@ import { AnnotationStyle, } from '@superset-ui/core'; import * as toastActions from 'src/components/MessageToasts/actions'; -import { LOG_EVENT } from 'src/logger/actions'; import * as exploreUtils from 'src/explore/exploreUtils'; import * as actions from 'src/components/Chart/chartAction'; import * as asyncEvent from 'src/middleware/asyncEvent'; @@ -100,7 +99,7 @@ describe('chart actions', () => { let getChartDataUriStub: jest.SpyInstance; let buildV1ChartDataPayloadStub: jest.SpyInstance; let waitForAsyncDataStub: jest.SpyInstance; - let fakeMetadata: { useLegacyApi?: boolean; viz_type?: string }; + let fakeMetadata: { viz_type?: string }; beforeAll(() => { fetchMock.get('glob:*api/v1/security/csrf_token/*', { result: '1234' }); @@ -135,7 +134,7 @@ describe('chart actions', () => { } as unknown as Awaited< ReturnType >); - fakeMetadata = { useLegacyApi: true }; + fakeMetadata = {}; mockedGetChartMetadataRegistry.mockImplementation( () => ({ @@ -322,7 +321,7 @@ describe('chart actions', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('v1 API', () => { beforeEach(() => { - fakeMetadata = { viz_type: 'my_viz', useLegacyApi: false }; + fakeMetadata = { viz_type: 'my_viz' }; }); test('should query with the built query', async () => { @@ -572,221 +571,6 @@ describe('chart actions', () => { }); }); - // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks - describe('legacy API', () => { - beforeEach(() => { - fakeMetadata = { useLegacyApi: true }; - }); - - test('should dispatch CHART_UPDATE_STARTED action before the query', () => { - const actionThunk = actions.postChartFormData({ - viz_type: 'table', - } as QueryFormData); - - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, success - expect(dispatch.mock.calls.length).toBe(5); - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.mock.calls[0][0].type).toBe( - actions.CHART_UPDATE_STARTED, - ); - }); - }); - - test('should dispatch TRIGGER_QUERY action with the query', () => { - const actionThunk = actions.postChartFormData({ - viz_type: 'table', - } as QueryFormData); - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, success - expect(dispatch.mock.calls.length).toBe(5); - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.mock.calls[1][0].type).toBe(actions.TRIGGER_QUERY); - }); - }); - - test('should dispatch UPDATE_QUERY_FORM_DATA action with the query', () => { - const actionThunk = actions.postChartFormData({ - viz_type: 'table', - } as QueryFormData); - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, success - expect(dispatch.mock.calls.length).toBe(5); - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.mock.calls[2][0].type).toBe( - actions.UPDATE_QUERY_FORM_DATA, - ); - }); - }); - - test('should dispatch logEvent async action', () => { - const actionThunk = actions.postChartFormData({ - viz_type: 'table', - } as QueryFormData); - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, success - expect(dispatch.mock.calls.length).toBe(5); - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(typeof dispatch.mock.calls[3][0]).toBe('function'); - - dispatch.mock.calls[3][0](dispatch); - expect(dispatch.mock.calls.length).toBe(6); - expect(dispatch.mock.calls[5][0].type).toBe(LOG_EVENT); - }); - }); - - test('should dispatch CHART_UPDATE_SUCCEEDED action upon success', () => { - // Pass a viz_type so getQuerySettings returns useLegacyApi from the mocked registry - const actionThunk = actions.postChartFormData({ - viz_type: 'table', - } as QueryFormData); - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, success - expect(dispatch.mock.calls.length).toBe(5); - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.mock.calls[4][0].type).toBe( - actions.CHART_UPDATE_SUCCEEDED, - ); - }); - }); - - test('should dispatch CHART_UPDATE_FAILED action upon query timeout', () => { - const unresolvingPromise = new Promise(() => {}); - fetchMock.removeRoute(MOCK_URL); - fetchMock.post(MOCK_URL, () => unresolvingPromise, { - name: MOCK_URL, - }); - - const timeoutInSec = 1 / 1000; - const actionThunk = actions.postChartFormData( - {} as QueryFormData, - false, - timeoutInSec, - ); - - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, fail - expect(fetchMock.callHistory.calls(MOCK_URL)).toHaveLength(1); - expect(dispatch.mock.calls.length).toBe(5); - expect(dispatch.mock.calls[4][0].type).toBe( - actions.CHART_UPDATE_FAILED, - ); - - fetchMock.removeRoute(MOCK_URL); - setupDefaultFetchMock(); - }); - }); - - test('should dispatch CHART_UPDATE_FAILED action upon non-timeout non-abort failure', () => { - fetchMock.removeRoute(MOCK_URL); - fetchMock.post( - MOCK_URL, - { throws: { statusText: 'misc error' } }, - { name: MOCK_URL }, - ); - - const timeoutInSec = 100; // Set to a time that is longer than the time this will take to fail - const actionThunk = actions.postChartFormData( - {} as QueryFormData, - false, - timeoutInSec, - ); - - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - // chart update, trigger query, update form data, fail - expect(dispatch.mock.calls.length).toBe(5); - const updateFailedAction = dispatch.mock.calls[4][0]; - expect(updateFailedAction.type).toBe(actions.CHART_UPDATE_FAILED); - expect(updateFailedAction.queriesResponse[0].error).toBe('misc error'); - - fetchMock.removeRoute(MOCK_URL); - setupDefaultFetchMock(); - }); - }); - - test('should dispatch CHART_UPDATE_STOPPED action upon abort', () => { - fetchMock.removeRoute(MOCK_URL); - fetchMock.post( - MOCK_URL, - { throws: { name: 'AbortError' } }, - { name: MOCK_URL }, - ); - - const timeoutInSec = 100; - const actionThunk = actions.postChartFormData( - {} as QueryFormData, - false, - timeoutInSec, - ); - - return actionThunk( - dispatch as unknown as actions.ChartThunkDispatch, - mockGetState as unknown as () => actions.RootState, - undefined, - ).then(() => { - const types = dispatch.mock.calls - .map((call: [{ type?: string }]) => call[0] && call[0].type) - .filter(Boolean); - - expect(types).toContain(actions.CHART_UPDATE_STOPPED); - expect(types).not.toContain(actions.CHART_UPDATE_FAILED); - - fetchMock.removeRoutes(); - setupDefaultFetchMock(); - }); - }); - - test('should handle the bigint without regression', async () => { - getExploreUrlStub.mockRestore(); - const mockBigIntUrl = '/mock/chart/data/bigint'; - const expectedBigNumber = '9223372036854775807'; - fetchMock.post(mockBigIntUrl, `{ "value": ${expectedBigNumber} }`, { - name: mockBigIntUrl, - }); - getExploreUrlStub = jest - .spyOn(exploreUtils, 'getExploreUrl') - .mockImplementation(() => mockBigIntUrl); - - // Need viz_type to trigger the mocked getChartMetadataRegistry for legacy API - const { json } = await actions.getChartDataRequest({ - formData: { ...fakeMetadata, viz_type: 'table' } as QueryFormData, - }); - - expect(fetchMock.callHistory.calls(mockBigIntUrl)).toHaveLength(1); - expect((json.result[0] as JsonObject).value.toString()).toEqual( - expectedBigNumber, - ); - }); - }); - // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('runAnnotationQuery', () => { const mockDispatch = jest.fn(); diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx index e0375f9d67f5..ab9afb3beabe 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/ExploreChartPanel.test.tsx @@ -90,7 +90,6 @@ describe('ChartContainer', () => { new ChartMetadata({ name: 'fake table', thumbnail: '.png', - useLegacyApi: false, }), ); render(, { useRedux: true }); diff --git a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx index 34ba2d5fc540..b9b889aaa0ee 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel/index.tsx @@ -21,17 +21,14 @@ import { useDispatch, useSelector } from 'react-redux'; import Split from 'react-split'; import { t } from '@apache-superset/core/translation'; import { - DatasourceType, ensureIsArray, isFeatureEnabled, FeatureFlag, - getChartMetadataRegistry, SupersetClient, QueryFormData, JsonObject, getExtensionsRegistry, } from '@superset-ui/core'; -import { Alert } from '@apache-superset/core/components'; import { css, styled, useTheme } from '@apache-superset/core/theme'; import ChartContainer from 'src/components/Chart/ChartContainer'; import { updateExploreChartState } from 'src/explore/actions/exploreActions'; @@ -44,8 +41,6 @@ import { setItem, LocalStorageKeys, } from 'src/utils/localStorageHelpers'; -import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; -import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import { buildV1ChartDataPayload } from 'src/explore/exploreUtils'; import { getChartRequiredFieldsMissingMessage } from 'src/utils/getChartRequiredFieldsMissingMessage'; import type { ChartState, Datasource } from 'src/explore/types'; @@ -223,17 +218,10 @@ const ExploreChartPanel = ({ : getItem(LocalStorageKeys.IsDatapanelOpen, false), ); - const [showDatasetModal, setShowDatasetModal] = useState(false); - - const metaDataRegistry = getChartMetadataRegistry(); - const { useLegacyApi } = metaDataRegistry.get(vizType) ?? {}; - const vizTypeNeedsDataset = - useLegacyApi && datasource.type !== DatasourceType.Table; // added boolean column to below show boolean so that the errors aren't overlapping const showAlertBanner = !chartAlert && chartIsStale && - !vizTypeNeedsDataset && chart.chartStatus !== 'failed' && ensureIsArray(chart.queriesResponse).length > 0; @@ -583,17 +571,6 @@ const ExploreChartPanel = ({ queriesResponse={chart.queriesResponse} /> - {showDatasetModal && ( - setShowDatasetModal(false)} - buttonTextOnSave={t('Save')} - buttonTextOnOverwrite={t('Overwrite')} - datasource={getDatasourceAsSaveableDataset(datasource)} - openWindow={false} - formData={formData} - /> - )} ); }; diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx index 24edcc446176..6d29a65598aa 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/ExploreViewContainer.test.tsx @@ -260,7 +260,6 @@ test('generates a new form_data param when none is available', async () => { new ChartMetadata({ name: 'fake table', thumbnail: '.png', - useLegacyApi: false, }), ); const history = createMemoryHistory({ initialEntries: [defaultPath] }); @@ -333,7 +332,6 @@ test('doesnt call replace when pathname is not /explore', async () => { new ChartMetadata({ name: 'fake table', thumbnail: '.png', - useLegacyApi: false, }), ); const history = createMemoryHistory({ initialEntries: ['/dashboard'] }); diff --git a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx index 4625fff6c891..478eba7ace48 100644 --- a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx +++ b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.tsx @@ -66,7 +66,7 @@ describe('exploreUtils', () => { force: false, curUrl: 'http://superset.com', }); - compareURI(URI(url!), URI('/explore_json/')); + compareURI(URI(url!), URI('/explore/')); }); test('generates proper json forced url', () => { const url = getExploreUrl({ @@ -75,16 +75,7 @@ describe('exploreUtils', () => { force: true, curUrl: 'superset.com', }); - compareURI(URI(url!), URI('/explore_json/').search({ force: 'true' })); - }); - test('generates proper csv URL', () => { - const url = getExploreUrl({ - formData, - endpointType: 'csv', - force: false, - curUrl: 'superset.com', - }); - compareURI(URI(url!), URI('/explore_json/').search({ csv: 'true' })); + compareURI(URI(url!), URI('/explore/').search({ force: 'true' })); }); test('generates proper standalone URL', () => { const url = getExploreUrl({ @@ -107,7 +98,7 @@ describe('exploreUtils', () => { force: false, curUrl: 'superset.com?foo=bar', }); - compareURI(URI(url!), URI('/explore_json/').search({ foo: 'bar' })); + compareURI(URI(url!), URI('/explore/').search({ foo: 'bar' })); }); test('generate proper save slice url', () => { const url = getExploreUrl({ @@ -116,7 +107,7 @@ describe('exploreUtils', () => { force: false, curUrl: 'superset.com?foo=bar', }); - compareURI(URI(url!), URI('/explore_json/').search({ foo: 'bar' })); + compareURI(URI(url!), URI('/explore/').search({ foo: 'bar' })); }); }); @@ -202,49 +193,33 @@ describe('exploreUtils', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('getQuerySettings', () => { beforeAll(() => { - getChartMetadataRegistry() - .registerValue('my_legacy_viz', { - useLegacyApi: true, - } as unknown as ChartMetadata) - .registerValue('my_v1_viz', { - useLegacyApi: false, - } as unknown as ChartMetadata); + getChartMetadataRegistry().registerValue('my_custom_parse_viz', { + parseMethod: 'json', + } as unknown as ChartMetadata); }); afterAll(() => { - getChartMetadataRegistry().remove('my_legacy_viz').remove('my_v1_viz'); + getChartMetadataRegistry().remove('my_custom_parse_viz'); }); - test('returns true for legacy viz', () => { - const [useLegacyApi, parseMethod] = getQuerySettings({ + test('returns the parse method for a registered viz', () => { + const [parseMethod] = getQuerySettings({ ...formData, - viz_type: 'my_legacy_viz', + viz_type: 'my_custom_parse_viz', }); - expect(useLegacyApi).toBe(true); - expect(parseMethod).toBe('json-bigint'); - }); - - test('returns false for v1 viz', () => { - const [useLegacyApi, parseMethod] = getQuerySettings({ - ...formData, - viz_type: 'my_v1_viz', - }); - expect(useLegacyApi).toBe(false); - expect(parseMethod).toBe('json-bigint'); + expect(parseMethod).toBe('json'); }); - test('returns false for formData with unregistered viz_type', () => { - const [useLegacyApi, parseMethod] = getQuerySettings({ + test('defaults the parse method for unregistered viz types', () => { + const [parseMethod] = getQuerySettings({ ...formData, viz_type: 'undefined_viz', }); - expect(useLegacyApi).toBe(false); expect(parseMethod).toBe('json-bigint'); }); - test('returns false for formData without viz_type', () => { - const [useLegacyApi, parseMethod] = getQuerySettings(formData); - expect(useLegacyApi).toBe(false); + test('defaults the parse method without a viz_type', () => { + const [parseMethod] = getQuerySettings(formData); expect(parseMethod).toBe('json-bigint'); }); }); diff --git a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts index 3311e92c7619..d939b043f608 100644 --- a/superset-frontend/src/explore/exploreUtils/exportChart.test.ts +++ b/superset-frontend/src/explore/exploreUtils/exportChart.test.ts @@ -56,7 +56,6 @@ const createMockExportResponse = (headers: Headers = new Headers()) => ({ blob: jest.fn().mockResolvedValue(mockBlob), }); -// Minimal formData that won't trigger legacy API (useLegacyApi = false) const baseFormData = { datasource: '1__table', viz_type: 'table', @@ -179,138 +178,6 @@ test('exportChart passes xlsx exportType for Excel exports', async () => { ); }); -test('exportChart legacy API (useLegacyApi=true) passes prefixed URL to onStartStreamingExport when app root is configured', async () => { - const appRoot = '/superset'; - ensureAppRoot.mockImplementation((path: string) => `${appRoot}${path}`); - - getChartMetadataRegistry.mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), - }); - - const onStartStreamingExport = jest.fn(); - const legacyFormData = { - datasource: '1__table', - viz_type: 'legacy_viz', - }; - - await exportChart({ - formData: legacyFormData, - resultFormat: 'csv', - onStartStreamingExport: onStartStreamingExport as unknown as null, - }); - - expect(onStartStreamingExport).toHaveBeenCalledTimes(1); - const callArgs = onStartStreamingExport.mock.calls[0][0]; - // post `Superset.route_base = ""`, the blueprint path is - // `/explore_json/`. With appRoot=/superset, the prefixed URL is - // `/superset/explore_json/?csv=true` — single-prefix, not the legacy - // doubled `/superset/superset/explore_json/...` that this test used to - // pin (which was the bug, now fixed at source). - expect(callArgs.url).toBe('/superset/explore_json/?csv=true'); - expect(callArgs.exportType).toBe('csv'); -}); - -test('exportChart legacy API builds relative URL for CSV export without app root', async () => { - ensureAppRoot.mockImplementation((path: string) => path); - - getChartMetadataRegistry.mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), - }); - - const onStartStreamingExport = jest.fn(); - const legacyFormData = { - datasource: '1__table', - viz_type: 'world_map', - }; - - await exportChart({ - formData: legacyFormData, - resultFormat: 'csv', - onStartStreamingExport: onStartStreamingExport as unknown as null, - }); - - expect(onStartStreamingExport).toHaveBeenCalledTimes(1); - const callArgs = onStartStreamingExport.mock.calls[0][0]; - expect(callArgs.url).toBe('/explore_json/?csv=true'); -}); - -test('exportChart legacy API builds relative URL for xlsx export', async () => { - ensureAppRoot.mockImplementation((path: string) => path); - - getChartMetadataRegistry.mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), - }); - - const onStartStreamingExport = jest.fn(); - const legacyFormData = { - datasource: '1__table', - viz_type: 'bubble', - }; - - await exportChart({ - formData: legacyFormData, - resultFormat: 'xlsx', - resultType: 'results', - onStartStreamingExport: onStartStreamingExport as unknown as null, - }); - - expect(onStartStreamingExport).toHaveBeenCalledTimes(1); - const callArgs = onStartStreamingExport.mock.calls[0][0]; - expect(callArgs.url).toBe('/explore_json/?xlsx=true'); -}); - -test('exportChart legacy API calls postBlob with relative URL', async () => { - const { SupersetClient } = jest.requireMock('@superset-ui/core'); - ensureAppRoot.mockImplementation((path: string) => path); - SupersetClient.postBlob.mockResolvedValue(createMockExportResponse()); - - getChartMetadataRegistry.mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), - }); - - const legacyFormData = { - datasource: '1__table', - viz_type: 'world_map', - }; - - await exportChart({ - formData: legacyFormData, - resultFormat: 'csv', - resultType: 'full', - }); - - expect(SupersetClient.postBlob).toHaveBeenCalledTimes(1); - const [url] = SupersetClient.postBlob.mock.calls[0]; - expect(url).toBe('/explore_json/?csv=true'); - expect(url).not.toMatch(/^https?:\/\//); - expect(downloadBlob).toHaveBeenCalled(); -}); - -test('exportChart legacy API includes force param when force=true', async () => { - ensureAppRoot.mockImplementation((path: string) => path); - - getChartMetadataRegistry.mockReturnValue({ - get: jest.fn().mockReturnValue({ useLegacyApi: true, parseMethod: 'json' }), - }); - - const onStartStreamingExport = jest.fn(); - const legacyFormData = { - datasource: '1__table', - viz_type: 'world_map', - }; - - await exportChart({ - formData: legacyFormData, - resultFormat: 'csv', - force: true, - onStartStreamingExport: onStartStreamingExport as unknown as null, - }); - - expect(onStartStreamingExport).toHaveBeenCalledTimes(1); - const callArgs = onStartStreamingExport.mock.calls[0][0]; - expect(callArgs.url).toBe('/explore_json/?force=true&csv=true'); -}); - test('exportChart successfully exports chart as CSV', async () => { const { SupersetClient } = jest.requireMock('@superset-ui/core'); const mockResponse = createMockExportResponse(); diff --git a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts index 782d44128634..de1f7a38c700 100644 --- a/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getExploreUrl.test.ts @@ -36,44 +36,22 @@ test('Get ExploreUrl with default params', () => { expect(getExploreUrl(params)).toBe('http://localhost/explore/'); }); -test('Get ExploreUrl with endpointType:full', () => { +test('Get ExploreUrl for any endpoint type resolves to the explore page', () => { const params = createParams(); expect(getExploreUrl({ ...params, endpointType: 'full' })).toBe( - 'http://localhost/explore_json/', + 'http://localhost/explore/', ); }); -test('Get ExploreUrl with endpointType:full and method:GET', () => { - const params = createParams(); - expect( - getExploreUrl({ ...params, endpointType: 'full', method: 'GET' }), - ).toBe('http://localhost/explore_json/'); -}); - -test('Get relative ExploreUrl with endpointType:csv', () => { - const params = createParams(); - expect( - getExploreUrl({ ...params, endpointType: 'csv', relative: true }), - ).toBe('/explore_json/?csv=true'); -}); - -test('Get relative ExploreUrl with endpointType:xlsx', () => { - const params = createParams(); - expect( - getExploreUrl({ ...params, endpointType: 'xlsx', relative: true }), - ).toBe('/explore_json/?xlsx=true'); -}); - test('Get relative ExploreUrl with force:true', () => { const params = createParams(); expect( getExploreUrl({ ...params, - endpointType: 'csv', force: true, relative: true, }), - ).toBe('/explore_json/?force=true&csv=true'); + ).toBe('/explore/?force=true'); }); test('Get relative ExploreUrl with endpointType:base', () => { diff --git a/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts b/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts deleted file mode 100644 index eca145a680d9..000000000000 --- a/superset-frontend/src/explore/exploreUtils/getLegacyEndpointType.test.ts +++ /dev/null @@ -1,40 +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 { getLegacyEndpointType } from '.'; - -const createParams = () => ({ - resultType: 'resultType', - resultFormat: 'resultFormat', -}); - -test('Should return resultType when resultFormat!=csv', () => { - expect(getLegacyEndpointType(createParams())).toBe('resultType'); -}); - -test('Should return resultFormat when resultFormat:csv', () => { - expect( - getLegacyEndpointType({ ...createParams(), resultFormat: 'csv' }), - ).toBe('csv'); -}); - -test('Should return resultFormat when resultFormat:xlsx', () => { - expect( - getLegacyEndpointType({ ...createParams(), resultFormat: 'xlsx' }), - ).toBe('xlsx'); -}); diff --git a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts index 7d3b004cd4de..c9789c8d9daf 100644 --- a/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts +++ b/superset-frontend/src/explore/exploreUtils/getURIDirectory.test.ts @@ -18,17 +18,9 @@ */ import { getURIDirectory } from '.'; -test('Cases in which the "explore_json" will be returned', () => { - // post `Superset.route_base = ""` collapse the legacy - // `/superset/explore_json/` endpoint is gone; `getURIDirectory` now - // returns the bare `/explore_json/` path (appRoot is applied at the - // outer `ensureAppRoot` layer when `includeAppRoot=true`). - ['full', 'json', 'csv', 'query', 'results', 'samples'].forEach(name => { - expect(getURIDirectory(name)).toBe('/explore_json/'); +test('always returns the explore directory', () => { + ['full', 'json', 'any-string'].forEach(name => { + expect(getURIDirectory(name)).toBe('/explore/'); }); -}); - -test('Cases in which the "explore" will be returned', () => { - expect(getURIDirectory('any-string')).toBe('/explore/'); expect(getURIDirectory()).toBe('/explore/'); }); diff --git a/superset-frontend/src/explore/exploreUtils/index.ts b/superset-frontend/src/explore/exploreUtils/index.ts index 3e1cd406fbef..f4fc374c143a 100644 --- a/superset-frontend/src/explore/exploreUtils/index.ts +++ b/superset-frontend/src/explore/exploreUtils/index.ts @@ -156,19 +156,7 @@ export function getURIDirectory( endpointType: EndpointType | string = 'base', includeAppRoot = true, ): string { - // Building the directory part of the URI - const uri = [ - 'full', - 'json', - 'csv', - 'xlsx', - 'query', - 'results', - 'samples', - ].includes(endpointType) - ? '/explore_json/' - : '/explore/'; - return includeAppRoot ? ensureAppRoot(uri) : uri; + return includeAppRoot ? ensureAppRoot('/explore/') : '/explore/'; } export function mountExploreUrl( @@ -267,24 +255,9 @@ export function getExploreUrl({ if (force) { search.force = 'true'; } - if (endpointType === 'csv') { - search.csv = 'true'; - } - if (endpointType === 'xlsx') { - search.xlsx = 'true'; - } if (endpointType === URL_PARAMS.standalone.name) { search.standalone = '1'; } - if (endpointType === 'query') { - search.query = 'true'; - } - if (endpointType === 'results') { - search.results = 'true'; - } - if (endpointType === 'samples') { - search.samples = 'true'; - } const paramNames = Object.keys(requestParams); if (paramNames.length) { paramNames.forEach(name => { @@ -298,14 +271,11 @@ export function getExploreUrl({ export const getQuerySettings = ( formData: Partial, -): [boolean, string] => { +): [string] => { const vizMetadata = formData.viz_type ? getChartMetadataRegistry().get(formData.viz_type) : undefined; - return [ - vizMetadata?.useLegacyApi ?? false, - vizMetadata?.parseMethod ?? 'json-bigint', - ]; + return [vizMetadata?.parseMethod ?? 'json-bigint']; }; export const buildV1ChartDataPayload = async ({ @@ -346,15 +316,6 @@ export const buildV1ChartDataPayload = async ({ ); }; -export const getLegacyEndpointType = ({ - resultType, - resultFormat, -}: { - resultType: string; - resultFormat: string; -}): string => - resultFormat === 'csv' || resultFormat === 'xlsx' ? resultFormat : resultType; - export const exportChart = async ({ formData, resultFormat = 'json', @@ -363,30 +324,14 @@ export const exportChart = async ({ ownState = {}, onStartStreamingExport = null, }: ExportChartParams): Promise => { - let url: string | null; - let payload: QueryFormData | ReturnType; - const [useLegacyApi] = getQuerySettings(formData); - if (useLegacyApi) { - const endpointType = getLegacyEndpointType({ resultFormat, resultType }); - url = getExploreUrl({ - formData, - endpointType, - force, - allowDomainSharding: false, - relative: true, - includeAppRoot: false, - }); - payload = formData; - } else { - url = '/api/v1/chart/data'; - payload = await buildV1ChartDataPayload({ - formData, - force, - resultFormat, - resultType, - ownState, - }); - } + const url = '/api/v1/chart/data'; + const payload = await buildV1ChartDataPayload({ + formData, + force, + resultFormat, + resultType, + ownState, + }); // Check if streaming export handler is provided (from dashboard Chart.jsx) if (onStartStreamingExport) { diff --git a/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts b/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts deleted file mode 100644 index caeeae68787b..000000000000 --- a/superset-frontend/src/explore/exploreUtils/shouldUseLegacyApi.test.ts +++ /dev/null @@ -1,59 +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 { getChartMetadataRegistry } from '@superset-ui/core'; -import { getQuerySettings } from '.'; - -jest.mock('@superset-ui/core', () => ({ - ...jest.requireActual('@superset-ui/core'), - getChartMetadataRegistry: jest.fn(), -})); - -const mockedGetChartMetadataRegistry = getChartMetadataRegistry as jest.Mock; - -test('Should return false', () => { - const get = jest.fn(); - mockedGetChartMetadataRegistry.mockReturnValue({ get } as any); - expect(get).toHaveBeenCalledTimes(0); - const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' }); - expect(useLegacyApi).toBe(false); - expect(get).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledWith('name_test'); -}); - -test('Should return true', () => { - const get = jest.fn(); - get.mockReturnValue({ useLegacyApi: true }); - mockedGetChartMetadataRegistry.mockReturnValue({ get } as any); - expect(get).toHaveBeenCalledTimes(0); - const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' }); - expect(useLegacyApi).toBe(true); - expect(get).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledWith('name_test'); -}); - -test('Should return false when useLegacyApi:false', () => { - const get = jest.fn(); - get.mockReturnValue({ useLegacyApi: false }); - mockedGetChartMetadataRegistry.mockReturnValue({ get } as any); - expect(get).toHaveBeenCalledTimes(0); - const [useLegacyApi] = getQuerySettings({ viz_type: 'name_test' }); - expect(useLegacyApi).toBe(false); - expect(get).toHaveBeenCalledTimes(1); - expect(get).toHaveBeenCalledWith('name_test'); -}); diff --git a/superset/async_events/async_query_manager.py b/superset/async_events/async_query_manager.py index cda66bfde4aa..9fe5437f577b 100644 --- a/superset/async_events/async_query_manager.py +++ b/superset/async_events/async_query_manager.py @@ -121,7 +121,6 @@ def __init__(self) -> None: self._jwt_expiration_seconds: int = 0 self._load_chart_data_into_cache_job: Any = None # pylint: disable=invalid-name - self._load_explore_json_into_cache_job: Any = None def init_app(self, app: Flask) -> None: cache_type = app.config.get("CACHE_CONFIG", {}).get("CACHE_TYPE") @@ -162,13 +161,9 @@ def init_app(self, app: Flask) -> None: self.register_request_handlers(app) # pylint: disable=import-outside-toplevel - from superset.tasks.async_queries import ( - load_chart_data_into_cache, - load_explore_json_into_cache, - ) + from superset.tasks.async_queries import load_chart_data_into_cache self._load_chart_data_into_cache_job = load_chart_data_into_cache - self._load_explore_json_into_cache_job = load_explore_json_into_cache def register_request_handlers(self, app: Flask) -> None: @app.after_request @@ -285,32 +280,6 @@ def init_job(self, channel_id: str, user_id: Optional[int]) -> dict[str, Any]: channel_id, job_id, user_id, status=self.STATUS_PENDING ) - # pylint: disable=too-many-arguments - def submit_explore_json_job( - self, - channel_id: str, - form_data: dict[str, Any], - response_type: str, - force: Optional[bool] = False, - user_id: Optional[int] = None, - ) -> dict[str, Any]: - # pylint: disable=import-outside-toplevel - from superset import security_manager - - job_metadata = self.init_job(channel_id, user_id) - self._load_explore_json_into_cache_job.apply_async( - args=[ - {**job_metadata, "guest_token": guest_user.guest_token} - if (guest_user := security_manager.get_current_guest_user_if_guest()) - else job_metadata, - form_data, - response_type, - force, - ], - expires=self._jwt_expiration_seconds, - ) - return job_metadata - def submit_chart_data_job( self, channel_id: str, diff --git a/superset/charts/api.py b/superset/charts/api.py index 137c6dfac6e9..bac25f8131b7 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -1256,7 +1256,7 @@ def warm_up_cache(self) -> Response: Warms up the cache for the chart. Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON - encoded `logs.json` column associated with the `explore_json` action. + encoded `logs.json` column associated with the `explore` action. requestBody: description: >- Identifies the chart to warm up cache for, and any additional dashboard or diff --git a/superset/commands/chart/warm_up_cache.py b/superset/commands/chart/warm_up_cache.py index f36e1c16c718..11ff21683ad6 100644 --- a/superset/commands/chart/warm_up_cache.py +++ b/superset/commands/chart/warm_up_cache.py @@ -18,8 +18,6 @@ from typing import Any, cast, Optional, Union -from flask import g - from superset.commands.base import BaseCommand from superset.commands.chart.data.get_data_command import ChartDataCommand from superset.commands.chart.exceptions import ( @@ -33,8 +31,7 @@ from superset.models.slice import Slice from superset.utils import json from superset.utils.core import error_msg_from_exception, QueryObjectFilterClause -from superset.views.utils import get_dashboard_extra_filters, get_form_data, get_viz -from superset.viz import viz_types +from superset.views.utils import get_dashboard_extra_filters class ChartWarmUpCacheCommand(BaseCommand): @@ -58,27 +55,6 @@ def _get_dashboard_filters(self, chart_id: int) -> list[dict[str, Any]]: return get_dashboard_extra_filters(chart_id, self._dashboard_id) - def _warm_up_legacy_cache( - self, chart: Slice, form_data: dict[str, Any] - ) -> tuple[Any, Any]: - """Warm up cache for legacy visualizations.""" - if not chart.datasource: - raise ChartInvalidError("Chart's datasource does not exist") - - if self._dashboard_id: - form_data["extra_filters"] = self._get_dashboard_filters(chart.id) - - g.form_data = form_data - payload = get_viz( - datasource_type=chart.datasource.type, - datasource_id=chart.datasource.id, - form_data=form_data, - force=True, - ).get_payload() - delattr(g, "form_data") - - return payload["errors"] or None, payload["status"] - def _warm_up_non_legacy_cache(self, chart: Slice) -> tuple[Any, Any]: """Warm up cache for non-legacy visualizations.""" query_context = chart.get_query_context() @@ -112,12 +88,7 @@ def run(self) -> dict[str, Any]: chart = cast(Slice, self._chart_or_id) try: - form_data = get_form_data(chart.id, use_slice_data=True)[0] - - if form_data.get("viz_type") in viz_types: - error, status = self._warm_up_legacy_cache(chart, form_data) - else: - error, status = self._warm_up_non_legacy_cache(chart) + error, status = self._warm_up_non_legacy_cache(chart) except Exception as ex: # pylint: disable=broad-except error = error_msg_from_exception(ex) status = None diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index f90f57021d51..54e638219b4a 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -56,8 +56,6 @@ is_adhoc_metric, ) from superset.utils.pandas_postprocessing.utils import unescape_separator -from superset.views.utils import get_viz -from superset.viz import viz_types if TYPE_CHECKING: from superset.common.query_context import QueryContext @@ -632,29 +630,6 @@ def get_viz_annotation_data( # noqa: C901 ) try: - if chart.viz_type in viz_types: - if not chart.datasource: - raise QueryObjectValidationError( - _( - f"""The dataset for chart ID {chart.id} (referenced by - annotation layer '{annotation_layer["name"]}') was - not found. Please check that the dataset exists and - is accessible.""" - ) - ) - - form_data = chart.form_data.copy() - form_data.update(annotation_layer.get("overrides", {})) - - payload = get_viz( - datasource_type=chart.datasource.type, - datasource_id=chart.datasource.id, - form_data=form_data, - force=force, - ).get_payload() - - return payload["data"] - if not (query_context := chart.get_query_context()): raise QueryObjectValidationError( _( diff --git a/superset/config.py b/superset/config.py index 4eb87cea2e4c..1ce1bf0d0845 100644 --- a/superset/config.py +++ b/superset/config.py @@ -365,7 +365,6 @@ def _try_json_readsha(filepath: str, length: int) -> str | None: WTF_CSRF_EXEMPT_LIST = [ "superset.charts.data.api.data", "superset.dashboards.api.cache_dashboard_screenshot", - "superset.views.core.explore_json", "superset.views.core.log", "superset.views.datasource.views.samples", "flask_appbuilder.security.views.acs", diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 46be170e8684..1f1a7ee76b3b 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -1465,7 +1465,7 @@ def warm_up_cache(self) -> Response: Warms up the cache for the table. Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON - encoded `logs.json` column associated with the `explore_json` action. + encoded `logs.json` column associated with the `explore` action. requestBody: description: >- Identifies the database and table to warm up cache for, and any diff --git a/superset/middleware/legacy_prefix_redirect.py b/superset/middleware/legacy_prefix_redirect.py index 0bd83bd798bd..f63ad5bc562c 100644 --- a/superset/middleware/legacy_prefix_redirect.py +++ b/superset/middleware/legacy_prefix_redirect.py @@ -21,7 +21,7 @@ prefix off every view, bookmarks/email links/external integrations to the legacy paths would 404. This middleware closes that gap for one release cycle (removed at EOL ``5.0.0``, matching the ``@deprecated`` gate on -``Superset.explore`` and ``Superset.explore_json``). +``Superset.explore``). Layering invariant ------------------ @@ -104,12 +104,6 @@ "/file-handler": (frozenset({"GET"}), "/file-handler"), "/log/": (frozenset({"POST"}), "/log/"), "/sqllab/history/": (frozenset({"GET"}), "/sqllab/history/"), - # NOTE: the /explore_json[/data] canonicals below (Superset.explore_json / - # explore_json_data) are themselves @deprecated. When those endpoints are - # deleted these rows become redirects-to-404 — prune them (and their - # closed-set snapshot entries) in the same PR that removes the endpoints. - "/explore_json/": (frozenset({"GET", "POST"}), "/explore_json/"), - "/explore_json/data/": (frozenset({"GET"}), "/explore_json/data/"), # views/explore.py — ExploreView (route_base = "/explore") owns the # bare /explore/ rule; Superset.explore (deprecated) provides the # POST handler. ExplorePermalinkView.permalink (route_base = "") diff --git a/superset/models/slice.py b/superset/models/slice.py index bfdaaff8f861..8540817ee9fe 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -53,7 +53,6 @@ from superset.tasks.utils import get_current_user from superset.thumbnails.digest import get_chart_digest from superset.utils import core as utils, json -from superset.viz import BaseViz, viz_types if TYPE_CHECKING: from superset.common.query_context import QueryContext @@ -230,15 +229,6 @@ def datasource_edit_url(self) -> str | None: datasource = self.datasource return datasource.url if datasource else None - @property - def viz(self) -> BaseViz | None: - form_data = json.loads(self.params) - viz_class = viz_types.get(self.viz_type) - datasource = self.datasource - if viz_class and datasource: - return viz_class(datasource=datasource, form_data=form_data) - return None - @property def description_markeddown(self) -> str: return utils.markdown(self.description or "") @@ -249,8 +239,7 @@ def data(self) -> dict[str, Any]: data: dict[str, Any] = {} self.token = "" try: - viz = self.viz - data = viz.data if viz else self.form_data + data = self.form_data self.token = utils.get_form_data_token(data) except Exception as ex: # pylint: disable=broad-except logger.exception(ex) @@ -357,11 +346,6 @@ def slice_url(self) -> str: """Defines the url to access the slice""" return self.get_explore_url() - @property - def explore_json_url(self) -> str: - """Defines the url to access the slice""" - return self.get_explore_url("/explore_json") - @property def edit_url(self) -> str: return f"/chart/edit/{self.id}" diff --git a/superset/security/manager.py b/superset/security/manager.py index ff214c509089..434735365169 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -115,7 +115,6 @@ from superset.models.slice import Slice from superset.models.sql_lab import Query from superset.semantic_layers.models import SemanticLayer, SemanticView - from superset.viz import BaseViz logger = logging.getLogger(__name__) @@ -1435,7 +1434,6 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods ("can_read", "Chart"), ("can_dashboard", "Superset"), ("can_slice", "Superset"), - ("can_explore_json", "Superset"), ("can_dashboard_permalink", "Superset"), ("can_read", "DashboardPermalinkRestApi"), # Dashboard filter interactions @@ -3815,7 +3813,6 @@ def raise_for_access( # noqa: C901 query: Optional["Query | Explorable"] = None, query_context: Optional["QueryContext"] = None, table: Optional["Table"] = None, - viz: Optional["BaseViz"] = None, sql: Optional[str] = None, catalog: Optional[str] = None, schema: Optional[str] = None, @@ -3830,7 +3827,6 @@ def raise_for_access( # noqa: C901 :param query: The SQL Lab query :param query_context: The query context :param table: The Superset table (requires database) - :param viz: The visualization :param sql: The SQL string (requires database) :param catalog: Optional catalog name :param schema: Optional schema name @@ -4060,15 +4056,12 @@ def raise_for_access( # noqa: C901 ) ) - if datasource or query_context or viz: + if datasource or query_context: form_data = None if query_context: datasource = query_context.datasource form_data = query_context.form_data - elif viz: - datasource = viz.datasource - form_data = viz.form_data assert datasource diff --git a/superset/tasks/async_queries.py b/superset/tasks/async_queries.py index f9aa9efc2feb..ca20e676603b 100644 --- a/superset/tasks/async_queries.py +++ b/superset/tasks/async_queries.py @@ -16,10 +16,9 @@ # under the License. from __future__ import annotations -import copy import dataclasses import logging -from typing import Any, cast, TYPE_CHECKING +from typing import Any, TYPE_CHECKING from celery.exceptions import SoftTimeLimitExceeded from flask import current_app, g @@ -30,17 +29,13 @@ from superset.exceptions import ( SupersetErrorException, SupersetErrorsException, - SupersetVizException, ) from superset.extensions import ( async_query_manager, - cache_manager, celery_app, security_manager, ) -from superset.utils.cache import generate_cache_key, set_and_log_cache from superset.utils.core import override_user -from superset.views.utils import get_datasource_info, get_viz if TYPE_CHECKING: from superset.common.query_context import QueryContext @@ -122,77 +117,3 @@ def load_chart_data_into_cache( job_metadata, async_query_manager.STATUS_ERROR, errors=errors ) raise - - -@celery_app.task(name="load_explore_json_into_cache", soft_time_limit=query_timeout) -def load_explore_json_into_cache( # pylint: disable=too-many-locals - job_metadata: dict[str, Any], - form_data: dict[str, Any], - response_type: str | None = None, - force: bool = False, -) -> None: - cache_key_prefix = "ejr-" # ejr: explore_json request - - with override_user(_load_user_from_job_metadata(job_metadata), force=False): - try: - set_form_data(form_data) - datasource_id, datasource_type = get_datasource_info(None, None, form_data) - - # Perform a deep copy here so that below we can cache the original - # value of the form_data object. This is necessary since the viz - # objects modify the form_data object. If the modified version were - # to be cached here, it will lead to a cache miss when clients - # attempt to retrieve the value of the completed async query. - original_form_data = copy.deepcopy(form_data) - - viz_obj = get_viz( - datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, - form_data=form_data, - force=force, - ) - # run query & cache results - payload = viz_obj.get_payload() - if viz_obj.has_error(payload): - raise SupersetVizException(errors=payload["errors"]) - - # Cache the original form_data value for async retrieval - cache_value = { - "form_data": original_form_data, - "response_type": response_type, - } - cache_key = generate_cache_key(cache_value, cache_key_prefix) - cache_instance = cache_manager.cache - cache_timeout = ( - cache_instance.cache.default_timeout if cache_instance.cache else None - ) - set_and_log_cache( - cache_instance, cache_key, cache_value, cache_timeout=cache_timeout - ) - result_url = f"/explore_json/data/{cache_key}" - async_query_manager.update_job( - job_metadata, - async_query_manager.STATUS_DONE, - result_url=result_url, - ) - except SoftTimeLimitExceeded as ex: - logger.warning( - "A timeout occurred while loading explore json, error: %s", ex - ) - raise - except Exception as ex: - if isinstance(ex, SupersetVizException): - errors = ex.errors - # Extract SIP-40 style errors when available - elif isinstance(ex, SupersetErrorException): - errors = [dataclasses.asdict(ex.error)] # type: ignore - elif isinstance(ex, SupersetErrorsException): - errors = [dataclasses.asdict(error) for error in ex.errors] # type: ignore - else: - error = ex.message if hasattr(ex, "message") else str(ex) - errors = [error] # type: ignore - - async_query_manager.update_job( - job_metadata, async_query_manager.STATUS_ERROR, errors=errors - ) - raise diff --git a/superset/views/core.py b/superset/views/core.py index 37d13f0a1c26..72c808577224 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -39,7 +39,6 @@ from flask_appbuilder.security.decorators import ( has_access, has_access_api, - permission_name, ) from flask_babel import gettext as __, lazy_gettext as _ from sqlalchemy.exc import SQLAlchemyError @@ -51,7 +50,6 @@ is_feature_enabled, security_manager, ) -from superset.async_events.async_query_manager import AsyncQueryTokenException from superset.commands.chart.exceptions import ChartNotFoundError from superset.commands.chart.warm_up_cache import ChartWarmUpCacheCommand from superset.commands.dashboard.exceptions import DashboardAccessDeniedError @@ -60,19 +58,15 @@ from superset.commands.explore.form_data.get import GetFormDataCommand from superset.commands.explore.form_data.parameters import CommandParameters from superset.commands.explore.permalink.get import GetExplorePermalinkCommand -from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.connectors.sqla.models import BaseDatasource, SqlaTable from superset.daos.chart import ChartDAO from superset.daos.datasource import DatasourceDAO from superset.dashboards.permalink.exceptions import DashboardPermalinkGetFailedError from superset.exceptions import ( - CacheLoadError, - SupersetErrorException, SupersetException, SupersetSecurityException, ) from superset.explore.permalink.exceptions import ExplorePermalinkGetFailedError -from superset.extensions import async_query_manager, cache_manager from superset.models.core import Database from superset.models.dashboard import Dashboard from superset.models.slice import Slice @@ -83,10 +77,8 @@ ) from superset.tasks.utils import get_current_user from superset.utils import core as utils, json -from superset.utils.cache import etag_cache from superset.utils.core import ( DatasourceType, - GenericDataType, get_user_id, ReservedUrlParameters, ) @@ -94,28 +86,19 @@ api, BaseSupersetView, common_bootstrap_payload, - CsvResponse, - data_payload_response, deprecated, - generate_download_headers, json_error_response, json_success, - XlsxResponse, ) from superset.views.error_handling import handle_api_exception from superset.views.utils import ( bootstrap_user_data, - check_datasource_perms, - check_explore_cache_perms, - check_resource_permissions, get_datasource_info, get_explore_redirect_url, get_form_data, - get_viz, redirect_to_login, sanitize_datasource_data, ) -from superset.viz import BaseViz logger = logging.getLogger(__name__) @@ -159,245 +142,6 @@ def slice(self, slice_id: int) -> FlaskResponse: endpoint_params[ReservedUrlParameters.STANDALONE.value] = "true" return redirect(url_for("ExploreView.root", **endpoint_params)) - def get_query_string_response(self, viz_obj: BaseViz) -> FlaskResponse: - query = None - try: - if query_obj := viz_obj.query_obj(): - query = viz_obj.datasource.get_query_str(query_obj) - except Exception as ex: # pylint: disable=broad-except - err_msg = utils.error_msg_from_exception(ex) - logger.exception(err_msg) - return json_error_response(err_msg) - - if not query: - query = __("Query cannot be loaded.") - - return self.json_response( - {"query": query, "language": viz_obj.datasource.query_language} - ) - - def get_raw_results(self, viz_obj: BaseViz) -> FlaskResponse: - payload = viz_obj.get_df_payload() - if viz_obj.has_error(payload): - return json_error_response(payload=payload, status=400) - response = { - "data": payload["df"].to_dict("records") - if payload["df"] is not None - else [], - "colnames": payload.get("colnames"), - "coltypes": payload.get("coltypes"), - "rowcount": payload.get("rowcount"), - "sql_rowcount": payload.get("sql_rowcount"), - } - return self.json_response(response) - - def get_samples(self, viz_obj: BaseViz) -> FlaskResponse: - return self.json_response(viz_obj.get_samples()) - - @staticmethod - def send_data_payload_response(viz_obj: BaseViz, payload: Any) -> FlaskResponse: - return data_payload_response(*viz_obj.payload_json_and_has_error(payload)) - - def generate_json( - self, viz_obj: BaseViz, response_type: str | None = None - ) -> FlaskResponse: - if response_type == ChartDataResultFormat.CSV: - return CsvResponse( - viz_obj.get_csv(), headers=generate_download_headers("csv") - ) - - if response_type == ChartDataResultFormat.XLSX: - return self._generate_xlsx(viz_obj) - - if response_type == ChartDataResultType.QUERY: - return self.get_query_string_response(viz_obj) - - if response_type == ChartDataResultType.RESULTS: - return self.get_raw_results(viz_obj) - - if response_type == ChartDataResultType.SAMPLES: - return self.get_samples(viz_obj) - - payload = viz_obj.get_payload() - return self.send_data_payload_response(viz_obj, payload) - - @staticmethod - def _generate_xlsx(viz_obj: BaseViz) -> FlaskResponse: - import pandas as pd - - from superset.utils.excel import apply_column_types, df_to_excel - - payload = viz_obj.get_df_payload() - df = payload.get("df") - if df is None: - df = pd.DataFrame() - coltypes: list[GenericDataType] = [] - else: - coltypes = payload.get("coltypes") or [] - if coltypes: - df = apply_column_types(df, coltypes) - xlsx_data = df_to_excel(df, index=False) - return XlsxResponse(xlsx_data, headers=generate_download_headers("xlsx")) - - @event_logger.log_this - @api - @has_access_api - @handle_api_exception - @permission_name("explore_json") - @expose("/explore_json/data/", methods=("GET",)) - @check_resource_permissions(check_explore_cache_perms) - @deprecated(eol_version="5.0.0", new_target="/api/v1/chart/data/") - def explore_json_data(self, cache_key: str) -> FlaskResponse: - """Serves cached result data for async explore_json calls - - `self.generate_json` receives this input and returns different - payloads based on the request args in the first block - - TODO: form_data should not be loaded twice from cache - (also loaded in `check_explore_cache_perms`) - """ - try: - cached = cache_manager.cache.get(cache_key) - if not cached: - raise CacheLoadError("Cached data not found") - - form_data = cached.get("form_data") - response_type = cached.get("response_type") - # Set form_data in Flask Global as it is used as a fallback - # for async queries with jinja context - g.form_data = form_data - datasource_id, datasource_type = get_datasource_info(None, None, form_data) - - viz_obj = get_viz( - datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, - form_data=form_data, - force_cached=True, - ) - - return self.generate_json(viz_obj, response_type) - except SupersetErrorException: - # Let structured Superset errors (e.g. OAuth2RedirectError) propagate - # so the global Flask error handler serializes them. - raise - except SupersetException as ex: - return json_error_response(utils.error_msg_from_exception(ex), 400) - - @api - @has_access_api - @handle_api_exception - @event_logger.log_this - @expose( - "/explore_json///", - methods=( - "GET", - "POST", - ), - ) - @expose( - "/explore_json/", - methods=( - "GET", - "POST", - ), - ) - @etag_cache() - @check_resource_permissions(check_datasource_perms) - @deprecated(eol_version="5.0.0", new_target="/api/v1/chart/data") - def explore_json( # noqa: C901 - self, datasource_type: str | None = None, datasource_id: int | None = None - ) -> FlaskResponse: - """Serves all request that GET or POST form_data - - This endpoint evolved to be the entry point of many different - requests that GETs or POSTs a form_data. - - `self.generate_json` receives this input and returns different - payloads based on the request args in the first block - - TODO: break into one endpoint for each return shape""" - - response_type = ChartDataResultFormat.JSON.value - responses: list[ChartDataResultFormat | ChartDataResultType] = list( - ChartDataResultFormat - ) - responses.extend(list(ChartDataResultType)) - for response_option in responses: - if request.args.get(response_option) == "true": - response_type = response_option - break - - # Verify user has permission to export data files - if response_type in ( - ChartDataResultFormat.CSV, - ChartDataResultFormat.XLSX, - ): - if is_feature_enabled("GRANULAR_EXPORT_CONTROLS"): - can_export = security_manager.can_access("can_export_data", "Superset") - else: - can_export = security_manager.can_access("can_csv", "Superset") - if not can_export: - return json_error_response( - _("You don't have the rights to export data"), - status=403, - ) - - form_data = get_form_data()[0] - try: - datasource_id, datasource_type = get_datasource_info( - datasource_id, datasource_type, form_data - ) - force = request.args.get("force") == "true" - - # TODO: support CSV, SQL query and other non-JSON types - if ( - is_feature_enabled("GLOBAL_ASYNC_QUERIES") - and response_type == ChartDataResultFormat.JSON - ): - # First, look for the chart query results in the cache. - with contextlib.suppress(CacheLoadError): - viz_obj = get_viz( - datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, - form_data=form_data, - force_cached=True, - force=force, - ) - payload = viz_obj.get_payload() - # If the chart query has already been cached, return it immediately. - if payload is not None: - return self.send_data_payload_response(viz_obj, payload) - # Otherwise, kick off a background job to run the chart query. - # Clients will either poll or be notified of query completion, - # at which point they will call the /explore_json/data/ - # endpoint to retrieve the results. - try: - async_channel_id = ( - async_query_manager.parse_channel_id_from_request(request) - ) - job_metadata = async_query_manager.submit_explore_json_job( - async_channel_id, form_data, response_type, force, get_user_id() - ) - except AsyncQueryTokenException: - return json_error_response("Not authorized", 401) - - return json_success(json.dumps(job_metadata), status=202) - - viz_obj = get_viz( - datasource_type=cast(str, datasource_type), - datasource_id=datasource_id, - form_data=form_data, - force=force, - ) - - return self.generate_json(viz_obj, response_type) - except SupersetErrorException: - # Let structured Superset errors (e.g. OAuth2RedirectError) propagate - # so the global Flask error handler serializes them. - raise - except SupersetException as ex: - return json_error_response(utils.error_msg_from_exception(ex), 400) - @has_access @event_logger.log_this @expose( @@ -726,7 +470,7 @@ def warm_up_cache(self) -> FlaskResponse: Note for slices a force refresh occurs. In terms of the `extra_filters` these can be obtained from records in the JSON - encoded `logs.json` column associated with the `explore_json` action. + encoded `logs.json` column associated with the `explore` action. """ slice_id = request.args.get("slice_id") dashboard_id = request.args.get("dashboard_id") diff --git a/superset/views/utils.py b/superset/views/utils.py index 6fa24539701f..bea2cff2c1cb 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -34,19 +34,14 @@ from flask_appbuilder.security.sqla import models as ab_models from flask_appbuilder.security.sqla.models import User from flask_babel import _ -from sqlalchemy.exc import NoResultFound -from superset import appbuilder, dataframe, db, result_set, viz +from superset import appbuilder, dataframe, db, result_set from superset.common.db_query_status import QueryStatus -from superset.daos.datasource import DatasourceDAO -from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( - CacheLoadError, SerializationError, SupersetException, - SupersetSecurityException, ) -from superset.extensions import cache_manager, security_manager +from superset.extensions import feature_flag_manager, security_manager from superset.legacy import update_time_range from superset.models.core import Database from superset.models.dashboard import Dashboard @@ -60,7 +55,6 @@ from superset.utils import json from superset.utils.core import DatasourceType from superset.utils.decorators import stats_timing -from superset.viz import BaseViz logger = logging.getLogger(__name__) stats_logger = app.config["STATS_LOGGER"] @@ -164,24 +158,6 @@ def get_permissions( return roles_permissions, transformed_permissions -def get_viz( - form_data: FormData, - datasource_type: str, - datasource_id: int, - force: bool = False, - force_cached: bool = False, -) -> BaseViz: - viz_type = form_data.get("viz_type", "table") - datasource = DatasourceDAO.get_datasource( - DatasourceType(datasource_type), - datasource_id, - ) - viz_obj = viz.viz_types[viz_type]( - datasource, form_data=form_data, force=force, force_cached=force_cached - ) - return viz_obj - - def loads_request_json(request_json_data: str) -> dict[Any, Any]: """Parse a JSON request payload, coercing non-objects to ``{}``. @@ -617,81 +593,6 @@ def wrapper(*args: Any, **kwargs: Any) -> None: return decorator -def check_explore_cache_perms(_self: Any, cache_key: str) -> None: - """ - Loads async explore_json request data from cache and performs access check - - :param _self: the Superset view instance - :param cache_key: the cache key passed into /explore_json/data/ - :raises SupersetSecurityException: If the user cannot access the resource - """ - cached = cache_manager.cache.get(cache_key) - if not cached: - raise CacheLoadError("Cached data not found") - - check_datasource_perms(_self, form_data=cached["form_data"]) - - -def check_datasource_perms( - _self: Any, - datasource_type: Optional[str] = None, - datasource_id: Optional[int] = None, - **kwargs: Any, -) -> None: - """ - Check if user can access a cached response from explore_json. - - This function takes `self` since it must have the same signature as the - the decorated method. - - :param datasource_type: The datasource type - :param datasource_id: The datasource ID - :raises SupersetSecurityException: If the user cannot access the resource - """ - - form_data = kwargs["form_data"] if "form_data" in kwargs else get_form_data()[0] - - try: - datasource_id, datasource_type = get_datasource_info( - datasource_id, datasource_type, form_data - ) - except SupersetException as ex: - raise SupersetSecurityException( - SupersetError( - error_type=SupersetErrorType.FAILED_FETCHING_DATASOURCE_INFO_ERROR, - level=ErrorLevel.ERROR, - message=str(ex), - ) - ) from ex - - if datasource_type is None: - raise SupersetSecurityException( - SupersetError( - error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR, - level=ErrorLevel.ERROR, - message=_("Could not determine datasource type"), - ) - ) - - try: - viz_obj = get_viz( - datasource_type=datasource_type, - datasource_id=datasource_id, - form_data=form_data, - force=False, - ) - except NoResultFound as ex: - raise SupersetSecurityException( - SupersetError( - error_type=SupersetErrorType.UNKNOWN_DATASOURCE_TYPE_ERROR, - level=ErrorLevel.ERROR, - message=_("Could not find viz object"), - ) - ) from ex - - viz_obj.raise_for_access() - - def _deserialize_results_payload( payload: Union[bytes, str], query: Query, use_msgpack: Optional[bool] = False ) -> dict[str, Any]: diff --git a/superset/viz.py b/superset/viz.py deleted file mode 100644 index dddb8e698430..000000000000 --- a/superset/viz.py +++ /dev/null @@ -1,2732 +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. -# pylint: disable=too-many-lines -"""This module contains the 'Viz' objects - -These objects represent the backend of all the visualizations that -Superset can render. -""" - -from __future__ import annotations - -import copy -import dataclasses -import logging -import math -from collections import OrderedDict -from datetime import datetime, timedelta -from itertools import product -from typing import Any, cast, Optional, TYPE_CHECKING - -import numpy as np -import pandas as pd -import polyline -import pygeohash -from dateutil import relativedelta as rdelta -from deprecation import deprecated -from flask import current_app, request -from flask_babel import lazy_gettext as _ -from geopy.point import Point -from pandas.tseries.frequencies import to_offset - -from superset.common.db_query_status import QueryStatus -from superset.constants import CACHE_DISABLED_TIMEOUT -from superset.errors import ErrorLevel, SupersetError, SupersetErrorType -from superset.exceptions import ( - CacheLoadError, - NullValueException, - QueryObjectValidationError, - SpatialException, - SupersetErrorException, - SupersetSecurityException, -) -from superset.extensions import cache_manager, security_manager -from superset.models.helpers import QueryResult -from superset.superset_typing import ( - Column, - Metric, - QueryObjectDict, - VizData, - VizPayload, -) -from superset.utils import core as utils, csv, json -from superset.utils.cache import set_and_log_cache -from superset.utils.cache_keys import add_impersonation_cache_key_if_needed -from superset.utils.core import ( - apply_max_row_limit, - DateColumn, - DTTM_ALIAS, - ExtraFiltersReasonType, - get_column_name, - get_column_names, - get_column_names_from_columns, - JS_MAX_INTEGER, - merge_extra_filters, - simple_filter_to_adhoc, -) -from superset.utils.date_parser import get_since_until, parse_past_timedelta -from superset.utils.hashing import hash_from_str -from superset.utils.pandas_postprocessing.utils import ( - escape_separator, - FLAT_COLUMN_SEPARATOR, -) - -if TYPE_CHECKING: - from superset.connectors.sqla.models import BaseDatasource - -logger = logging.getLogger(__name__) - -METRIC_KEYS = [ - "metric", - "metrics", - "percent_metrics", - "metric_2", - "secondary_metric", - "x", - "y", - "size", -] - -# Allowlist of resampler aggregation methods that may be invoked dynamically via -# ``getattr`` on a pandas ``Resampler``. Restricting the set of callable names -# keeps the dynamic dispatch limited to known-safe aggregations. -ALLOWED_RESAMPLE_METHODS = frozenset( - { - "asfreq", - "bfill", - "count", - "ffill", - "first", - "last", - "max", - "mean", - "median", - "min", - "std", - "sum", - "var", - } -) - - -class BaseViz: # pylint: disable=too-many-public-methods - """All visualizations derive this base class""" - - viz_type: str | None = None - verbose_name = "Base Viz" - credits = "" - is_timeseries = False - cache_type = "df" - enforce_numerical_metrics = True - - @deprecated(deprecated_in="3.0") - def __init__( - self, - datasource: BaseDatasource, - form_data: dict[str, Any], - force: bool = False, - force_cached: bool = False, - ) -> None: - if not datasource: - raise QueryObjectValidationError(_("Viz is missing a datasource")) - - self.datasource = datasource - self.request = request - self.viz_type = form_data.get("viz_type") - self.form_data = form_data - - self.query = "" - self.token = utils.get_form_data_token(form_data) - - self.groupby: list[Column] = self.form_data.get("groupby") or [] - self.time_shift = timedelta() - - self.status: str | None = None - self.error_msg = "" - self.results: QueryResult | None = None - self.applied_filter_columns: list[Column] = [] - self.rejected_filter_columns: list[Column] = [] - self.errors: list[dict[str, Any]] = [] - self.force = force - self._force_cached = force_cached - self.from_dttm: datetime | None = None - self.to_dttm: datetime | None = None - self._extra_chart_data: list[tuple[str, pd.DataFrame]] = [] - - self.process_metrics() - - self.applied_filters: list[dict[str, str]] = [] - self.rejected_filters: list[dict[str, str]] = [] - - @property - @deprecated(deprecated_in="3.0") - def force_cached(self) -> bool: - return self._force_cached - - @deprecated(deprecated_in="3.0") - def process_metrics(self) -> None: - # metrics in Viz is order sensitive, so metric_dict should be - # OrderedDict - self.metric_dict = OrderedDict() - for mkey in METRIC_KEYS: - val = self.form_data.get(mkey) - if val: - if not isinstance(val, list): - val = [val] - for o in val: - label = utils.get_metric_name(o) - self.metric_dict[label] = o - - # Cast to list needed to return serializable object in py3 - self.all_metrics = list(self.metric_dict.values()) - self.metric_labels = list(self.metric_dict.keys()) - - @staticmethod - @deprecated(deprecated_in="3.0") - def handle_js_int_overflow( - data: dict[str, list[dict[str, Any]]], - ) -> dict[str, list[dict[str, Any]]]: - for record in data.get("records", {}): - for k, v in list(record.items()): - if isinstance(v, int): - # if an int is too big for Java Script to handle - # convert it to a string - if abs(v) > JS_MAX_INTEGER: - record[k] = str(v) - return data - - @deprecated(deprecated_in="3.0") - def run_extra_queries(self) -> None: - """Lifecycle method to use when more than one query is needed - - In rare-ish cases, a visualization may need to execute multiple - queries. That is the case for FilterBox or for time comparison - in Line chart for instance. - - In those cases, we need to make sure these queries run before the - main `get_payload` method gets called, so that the overall caching - metadata can be right. The way it works here is that if any of - the previous `get_df_payload` calls hit the cache, the main - payload's metadata will reflect that. - - The multi-query support may need more work to become a first class - use case in the framework, and for the UI to reflect the subtleties - (show that only some of the queries were served from cache for - instance). In the meantime, since multi-query is rare, we treat - it with a bit of a hack. Note that the hack became necessary - when moving from caching the visualization's data itself, to caching - the underlying query(ies). - """ - - @deprecated(deprecated_in="3.0") - def apply_rolling(self, df: pd.DataFrame) -> pd.DataFrame: - rolling_type = self.form_data.get("rolling_type") - rolling_periods = int(self.form_data.get("rolling_periods") or 0) - min_periods = int(self.form_data.get("min_periods") or 0) - - if rolling_type in ("mean", "std", "sum") and rolling_periods: - kwargs = {"window": rolling_periods, "min_periods": min_periods} - if rolling_type == "mean": - df = df.rolling(**kwargs).mean() - elif rolling_type == "std": - df = df.rolling(**kwargs).std() - elif rolling_type == "sum": - df = df.rolling(**kwargs).sum() - elif rolling_type == "cumsum": - df = df.cumsum() - if min_periods: - df = df[min_periods:] - if df.empty: - raise QueryObjectValidationError( - _( - "Applied rolling window did not return any data. Please make sure " - "the source query satisfies the minimum periods defined in the " - "rolling window." - ) - ) - return df - - @deprecated(deprecated_in="3.0") - def get_samples(self) -> dict[str, Any]: - query_obj = self.query_obj() - query_obj.update( - { - "is_timeseries": False, - "groupby": [], - "metrics": [], - "orderby": [], - "row_limit": current_app.config["SAMPLES_ROW_LIMIT"], - "columns": [o.column_name for o in self.datasource.columns], - "from_dttm": None, - "to_dttm": None, - } - ) - payload = self.get_df_payload(query_obj) # leverage caching logic - return { - "data": payload["df"].to_dict(orient="records"), - "colnames": payload.get("colnames"), - "coltypes": payload.get("coltypes"), - "rowcount": payload.get("rowcount"), - "sql_rowcount": payload.get("sql_rowcount"), - } - - @deprecated(deprecated_in="3.0") - def get_df(self, query_obj: QueryObjectDict | None = None) -> pd.DataFrame: - """Returns a pandas dataframe based on the query object""" - if not query_obj: - query_obj = self.query_obj() - if not query_obj: - return pd.DataFrame() - - self.error_msg = "" - - timestamp_format = None - if self.datasource.type == "table": - granularity_col = self.datasource.get_column(query_obj["granularity"]) - if granularity_col: - timestamp_format = granularity_col.python_date_format - - # The datasource here can be different backend but the interface is common - self.results = self.datasource.query(query_obj) - self.applied_filter_columns = self.results.applied_filter_columns or [] - self.rejected_filter_columns = self.results.rejected_filter_columns or [] - self.query = self.results.query - self.status = self.results.status - self.errors = self.results.errors - - df = self.results.df - # Transform the timestamp we received from database to pandas supported - # datetime format. If no python_date_format is specified, the pattern will - # be considered as the default ISO date format - # If the datetime format is unix, the parse will use the corresponding - # parsing logic. - if not df.empty: - utils.normalize_dttm_col( - df=df, - dttm_cols=tuple( # noqa: C409 - [ - DateColumn.get_legacy_time_column( - timestamp_format=timestamp_format, - offset=self.datasource.offset, - time_shift=self.form_data.get("time_shift"), - ) - ] - ), - ) - - if self.enforce_numerical_metrics: - self.df_metrics_to_num(df) - - df.replace([np.inf, -np.inf], np.nan, inplace=True) - return df - - @deprecated(deprecated_in="3.0") - def df_metrics_to_num(self, df: pd.DataFrame) -> None: - """Converting metrics to numeric when pandas.read_sql cannot""" - metrics = self.metric_labels - for col, dtype in df.dtypes.items(): - if dtype.type == np.object_ and col in metrics: - df[col] = pd.to_numeric(df[col], errors="coerce") - - @deprecated(deprecated_in="3.0") - def process_query_filters(self) -> None: - utils.convert_legacy_filters_into_adhoc(self.form_data) - merge_extra_filters(self.form_data) - engine = self.datasource.database.db_engine_spec.engine - utils.split_adhoc_filters_into_base_filters(self.form_data, engine) - - @staticmethod - @deprecated(deprecated_in="3.0") - def dedup_columns(*columns_args: list[Column] | None) -> list[Column]: - # dedup groupby and columns while preserving order - labels: list[str] = [] - deduped_columns: list[Column] = [] - for columns in columns_args: - for column in columns or []: - label = get_column_name(column) - if label not in labels: - deduped_columns.append(column) - return deduped_columns - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: # pylint: disable=too-many-locals - """Building a query object""" - self.process_query_filters() - - metrics = self.all_metrics or [] - - groupby = self.dedup_columns(self.groupby, self.form_data.get("columns")) - - is_timeseries = self.is_timeseries - - if DTTM_ALIAS in (groupby_labels := get_column_names(groupby)): - del groupby[groupby_labels.index(DTTM_ALIAS)] - is_timeseries = True - - granularity = self.form_data.get("granularity_sqla") - limit = int(self.form_data.get("limit") or 0) - timeseries_limit_metric = self.form_data.get("timeseries_limit_metric") - - # apply row limit to query - row_limit = int( - self.form_data.get("row_limit") or current_app.config["ROW_LIMIT"] - ) - row_limit = apply_max_row_limit(row_limit) - - # default order direction - order_desc = self.form_data.get("order_desc", True) - - try: - since, until = get_since_until( - relative_start=current_app.config["DEFAULT_RELATIVE_START_TIME"], - relative_end=current_app.config["DEFAULT_RELATIVE_END_TIME"], - time_range=self.form_data.get("time_range"), - since=self.form_data.get("since"), - until=self.form_data.get("until"), - ) - except ValueError as ex: - raise QueryObjectValidationError(str(ex)) from ex - - time_shift = self.form_data.get("time_shift", "") - self.time_shift = parse_past_timedelta(time_shift) - from_dttm = None if since is None else (since - self.time_shift) - to_dttm = None if until is None else (until - self.time_shift) - if from_dttm and to_dttm and from_dttm > to_dttm: - raise QueryObjectValidationError( - _("From date cannot be larger than to date") - ) - - self.from_dttm = from_dttm - self.to_dttm = to_dttm - - # extras are used to query elements specific to a datasource type - # for instance the extra where clause that applies only to Tables - extras = { - "having": self.form_data.get("having", ""), - "time_grain_sqla": self.form_data.get("time_grain_sqla"), - "where": self.form_data.get("where", ""), - } - - return { - "granularity": granularity, - "from_dttm": from_dttm, - "to_dttm": to_dttm, - "is_timeseries": is_timeseries, - "groupby": groupby, - "metrics": metrics, - "row_limit": row_limit, - "filter": self.form_data.get("filters", []), - "series_limit": limit, - "extras": extras, - "series_limit_metric": timeseries_limit_metric, - "order_desc": order_desc, - } - - @property - @deprecated(deprecated_in="3.0") - def cache_timeout(self) -> int: - if self.form_data.get("cache_timeout") is not None: - return int(self.form_data["cache_timeout"]) - if self.datasource.cache_timeout is not None: - return self.datasource.cache_timeout - if ( - hasattr(self.datasource, "database") - and self.datasource.database.cache_timeout - ) is not None: - return self.datasource.database.cache_timeout - if ( - current_app.config["DATA_CACHE_CONFIG"].get("CACHE_DEFAULT_TIMEOUT") - is not None - ): - return current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] - return current_app.config["CACHE_DEFAULT_TIMEOUT"] - - @deprecated(deprecated_in="3.0") - def get_json(self) -> str: - return json.dumps( - self.get_payload(), default=json.json_int_dttm_ser, ignore_nan=True - ) - - @deprecated(deprecated_in="3.0") - def cache_key(self, query_obj: QueryObjectDict, **extra: Any) -> str: - """ - The cache key is made out of the key/values in `query_obj`, plus any - other key/values in `extra`. - - We remove datetime bounds that are hard values, and replace them with - the use-provided inputs to bounds, which may be time-relative (as in - "5 days ago" or "now"). - - The `extra` arguments are currently used by time shift queries, since - different time shifts will differ only in the `from_dttm`, `to_dttm`, - `inner_from_dttm`, and `inner_to_dttm` values which are stripped. - """ - # Cast to dict[str, Any] to allow mutable operations (update, del) - # since TypedDict doesn't support these operations in the same way - cache_dict: dict[str, Any] = copy.copy(cast(dict[str, Any], query_obj)) - cache_dict.update(extra) - - for k in ["from_dttm", "to_dttm", "inner_from_dttm", "inner_to_dttm"]: - if k in cache_dict: - del cache_dict[k] - - cache_dict["time_range"] = self.form_data.get("time_range") - cache_dict["datasource"] = self.datasource.uid - cache_dict["extra_cache_keys"] = self.datasource.get_extra_cache_keys(query_obj) - cache_dict["rls"] = security_manager.get_rls_cache_key(self.datasource) - cache_dict["changed_on"] = self.datasource.changed_on - - # Add an impersonation key to cache if impersonation is enabled on the db - # or if the CACHE_QUERY_BY_USER flag is on or per_user_caching is enabled on - # the database - try: - add_impersonation_cache_key_if_needed(self.datasource.database, cache_dict) - except AttributeError: - # datasource or database do not exist - pass - - json_data = self.json_dumps(cache_dict, sort_keys=True) - return hash_from_str(json_data) - - @deprecated(deprecated_in="3.0") - def get_payload(self, query_obj: QueryObjectDict | None = None) -> VizPayload: - """Returns a payload of metadata and data""" - - try: - self.run_extra_queries() - except SupersetSecurityException as ex: - error = dataclasses.asdict(ex.error) - self.errors.append(error) - self.status = QueryStatus.FAILED - - payload = self.get_df_payload(query_obj) - - # if payload does not have a df, we are raising an error here. - df = cast(Optional[pd.DataFrame], payload["df"]) - - if self.status != QueryStatus.FAILED: - payload["data"] = self.get_data(df) - if "df" in payload: - del payload["df"] - - applied_filter_columns = self.applied_filter_columns or [] - rejected_filter_columns = self.rejected_filter_columns or [] - applied_time_extras = self.form_data.get("applied_time_extras", {}) - applied_time_columns, rejected_time_columns = utils.get_time_filter_status( - self.datasource, applied_time_extras - ) - payload["applied_filters"] = [ - {"column": get_column_name(col)} for col in applied_filter_columns - ] + applied_time_columns - payload["rejected_filters"] = [ - { - "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, - "column": get_column_name(col), - } - for col in rejected_filter_columns - ] + rejected_time_columns - if df is not None: - payload["colnames"] = list(df.columns) - - return payload - - @deprecated(deprecated_in="3.0") - def get_df_payload( # pylint: disable=too-many-statements # noqa: C901 - self, query_obj: QueryObjectDict | None = None, **kwargs: Any - ) -> dict[str, Any]: - """Handles caching around the df payload retrieval""" - if not query_obj: - query_obj = self.query_obj() - cache_key = self.cache_key(query_obj, **kwargs) if query_obj else None - cache_value = None - logger.info("Cache key: %s", cache_key) - is_loaded = False - stacktrace = None - df = None - cache_timeout = self.cache_timeout - force = self.force or cache_timeout == CACHE_DISABLED_TIMEOUT - if cache_key and cache_manager.data_cache and not force: - cache_value = cache_manager.data_cache.get(cache_key) - if cache_value: - current_app.config["STATS_LOGGER"].incr("loading_from_cache") - try: - df = cache_value["df"] - self.query = cache_value["query"] - self.applied_filter_columns = cache_value.get( - "applied_filter_columns", [] - ) - self.rejected_filter_columns = cache_value.get( - "rejected_filter_columns", [] - ) - self.status = QueryStatus.SUCCESS - is_loaded = True - current_app.config["STATS_LOGGER"].incr("loaded_from_cache") - except Exception as ex: # pylint: disable=broad-except - logger.exception(ex) - logger.error( - "Error reading cache: %s", - utils.error_msg_from_exception(ex), - exc_info=True, - ) - logger.info("Serving from cache") - - if query_obj and not is_loaded: - if self.force_cached: - logger.warning( - "force_cached (viz.py): value not found for cache key %s", - cache_key, - ) - raise CacheLoadError(_("Cached value not found")) - try: - invalid_columns = [ - col - for col in get_column_names_from_columns( - query_obj.get("columns") or [] - ) - + get_column_names_from_columns(query_obj.get("groupby") or []) - + utils.get_column_names_from_metrics( - cast(list[Metric], query_obj.get("metrics") or []) - ) - if col not in self.datasource.column_names - ] - if invalid_columns: - raise QueryObjectValidationError( - _( - "Columns missing in datasource: %(invalid_columns)s", - invalid_columns=invalid_columns, - ) - ) - df = self.get_df(query_obj) - if self.status != QueryStatus.FAILED: - current_app.config["STATS_LOGGER"].incr("loaded_from_source") - if not self.force: - current_app.config["STATS_LOGGER"].incr( - "loaded_from_source_without_force" - ) - is_loaded = True - except QueryObjectValidationError as ex: - error = dataclasses.asdict( - SupersetError( - message=str(ex), - level=ErrorLevel.ERROR, - error_type=SupersetErrorType.VIZ_GET_DF_ERROR, - ) - ) - self.errors.append(error) - self.status = QueryStatus.FAILED - except SupersetErrorException: - # Let structured Superset errors (e.g. OAuth2RedirectError) propagate - # so the global Flask error handler serializes them. - raise - except Exception as ex: # pylint: disable=broad-except - logger.exception(ex) - - error = dataclasses.asdict( - SupersetError( - message=str(ex), - level=ErrorLevel.ERROR, - error_type=SupersetErrorType.VIZ_GET_DF_ERROR, - ) - ) - self.errors.append(error) - self.status = QueryStatus.FAILED - # Only expose the raw stacktrace when explicitly enabled, mirroring - # the gating used elsewhere (e.g. superset.views.base.get_error_msg). - # ``get_stacktrace()`` itself returns ``None`` unless SHOW_STACKTRACE - # is set, so gating purely on that config keeps the two consistent. - if current_app.config.get("SHOW_STACKTRACE"): - stacktrace = utils.get_stacktrace() - - if is_loaded and cache_key and self.status != QueryStatus.FAILED: - set_and_log_cache( - cache_instance=cache_manager.data_cache, - cache_key=cache_key, - cache_value={"df": df, "query": self.query}, - cache_timeout=cache_timeout, - datasource_uid=self.datasource.uid, - ) - return { - "cache_key": cache_key, - "cached_dttm": cache_value["dttm"] if cache_value is not None else None, - "cache_timeout": cache_timeout, - "df": df, - "errors": self.errors, - "form_data": self.form_data, - "is_cached": cache_value is not None, - "query": self.query, - "from_dttm": self.from_dttm, - "to_dttm": self.to_dttm, - "status": self.status, - "stacktrace": stacktrace, - "rowcount": len(df.index) if df is not None else 0, - "colnames": list(df.columns) if df is not None else None, - "coltypes": ( - utils.extract_dataframe_dtypes(df, self.datasource) - if df is not None - else None - ), - } - - @staticmethod - @deprecated(deprecated_in="3.0") - def json_dumps(query_obj: Any, sort_keys: bool = False) -> str: - return json.dumps( - query_obj, - default=json.json_int_dttm_ser, - ignore_nan=True, - sort_keys=sort_keys, - ) - - @staticmethod - @deprecated(deprecated_in="3.0") - def has_error(payload: VizPayload) -> bool: - return ( - payload.get("status") == QueryStatus.FAILED - or payload.get("error") is not None - or bool(payload.get("errors")) - ) - - @deprecated(deprecated_in="3.0") - def payload_json_and_has_error(self, payload: VizPayload) -> tuple[str, bool]: - return self.json_dumps(payload), self.has_error(payload) - - @property - @deprecated(deprecated_in="3.0") - def data(self) -> dict[str, Any]: - """This is the data object serialized to the js layer""" - content = { - "form_data": self.form_data, - "token": self.token, - "viz_name": self.viz_type, - "filter_select_enabled": self.datasource.filter_select_enabled, - } - return content - - @deprecated(deprecated_in="3.0") - def get_csv(self) -> str | None: - df = self.get_df_payload()["df"] # leverage caching logic - include_index = not isinstance(df.index, pd.RangeIndex) - return csv.df_to_escaped_csv( - df, index=include_index, **current_app.config["CSV_EXPORT"] - ) - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - return df.to_dict(orient="records") - - @property - @deprecated(deprecated_in="3.0") - def json_data(self) -> str: - return json.dumps(self.data) - - @deprecated(deprecated_in="3.0") - def raise_for_access(self) -> None: - """ - Raise an exception if the user cannot access the resource. - - :raises SupersetSecurityException: If the user cannot access the resource - """ - - security_manager.raise_for_access(viz=self) - - -class TimeTableViz(BaseViz): - """A data table with rich time-series related columns""" - - viz_type = "time_table" - verbose_name = _("Time Table View") - credits = 'a Superset original' - is_timeseries = True - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - - if not self.form_data.get("metrics"): - raise QueryObjectValidationError(_("Pick at least one metric")) - - if self.form_data.get("groupby") and len(self.form_data["metrics"]) > 1: - raise QueryObjectValidationError( - _("When using 'Group By' you are limited to use a single metric") - ) - - sort_by = utils.get_first_metric_name(query_obj["metrics"]) - is_asc = not query_obj.get("order_desc") - query_obj["orderby"] = [(sort_by, is_asc)] - - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - columns = None - values: list[str] | str = self.metric_labels - if self.form_data.get("groupby"): - values = self.metric_labels[0] - columns = get_column_names(self.form_data.get("groupby")) - pt = df.pivot_table(index=DTTM_ALIAS, columns=columns, values=values) - pt.index = pt.index.map(str) - pt = pt.sort_index() - if isinstance(pt.columns, pd.MultiIndex): - pt.columns = [ - FLAT_COLUMN_SEPARATOR.join(escape_separator(str(s)) for s in col) - for col in pt.columns - ] - return { - "records": pt.to_dict(orient="index"), - "columns": list(pt.columns), - "is_group_by": bool(self.form_data.get("groupby")), - } - - -class CalHeatmapViz(BaseViz): - """Calendar heatmap.""" - - viz_type = "cal_heatmap" - verbose_name = _("Calendar Heatmap") - credits = "cal-heatmap" - is_timeseries = True - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: # pylint: disable=too-many-locals # noqa: C901 - if df.empty: - return None - - form_data = self.form_data - data = {} - records = df.to_dict("records") - for metric in self.metric_labels: - values = {} - for query_obj in records: - v = query_obj[DTTM_ALIAS] - if hasattr(v, "value"): - v = v.value - values[str(v / 10**9)] = query_obj.get(metric) - data[metric] = values - - try: - start, end = get_since_until( - relative_start=current_app.config["DEFAULT_RELATIVE_START_TIME"], - relative_end=current_app.config["DEFAULT_RELATIVE_END_TIME"], - time_range=form_data.get("time_range"), - since=form_data.get("since"), - until=form_data.get("until"), - ) - except ValueError as ex: - raise QueryObjectValidationError(str(ex)) from ex - if not start or not end: - raise QueryObjectValidationError( - "Please provide both time bounds (Since and Until)" - ) - domain = form_data.get("domain_granularity") - diff_delta = rdelta.relativedelta(end, start) - diff_secs = (end - start).total_seconds() - - if domain == "year": - range_ = end.year - start.year + 1 - elif domain == "month": - range_ = diff_delta.years * 12 + diff_delta.months + 1 - elif domain == "week": - range_ = diff_delta.years * 53 + diff_delta.weeks + 1 - elif domain == "day": - range_ = diff_secs // (24 * 60 * 60) + 1 # type: ignore - else: - range_ = diff_secs // (60 * 60) + 1 # type: ignore - - return { - "data": data, - "start": start, - "domain": domain, - "subdomain": form_data.get("subdomain_granularity"), - "range": range_, - } - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["metrics"] = self.form_data.get("metrics") - mapping = { - "min": "PT1M", - "hour": "PT1H", - "day": "P1D", - "week": "P1W", - "month": "P1M", - "year": "P1Y", - } - query_obj["extras"]["time_grain_sqla"] = mapping[ - self.form_data.get("subdomain_granularity", "min") - ] - return query_obj - - -class NVD3Viz(BaseViz): - """Base class for all nvd3 vizs""" - - credits = 'NVD3.org' - viz_type: str | None = None - verbose_name = "Base NVD3 Viz" - is_timeseries = False - - -class BulletViz(NVD3Viz): - """Based on the NVD3 bullet chart""" - - viz_type = "bullet" - verbose_name = _("Bullet Chart") - is_timeseries = False - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - form_data = self.form_data - query_obj = super().query_obj() - self.metric = form_data[ # pylint: disable=attribute-defined-outside-init - "metric" - ] - - query_obj["metrics"] = [self.metric] - if not self.metric: - raise QueryObjectValidationError(_("Pick a metric to display")) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - df["metric"] = df[[utils.get_metric_name(self.metric)]] - values = df["metric"].values - return { - "measures": values.tolist(), - } - - -class NVD3TimeSeriesViz(NVD3Viz): - """A rich line chart component with tons of options""" - - viz_type = "line" - verbose_name = _("Time Series - Line Chart") - sort_series = False - is_timeseries = True - pivot_fill_value: int | None = None - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - sort_by = self.form_data.get( - "timeseries_limit_metric" - ) or utils.get_first_metric_name(query_obj.get("metrics") or []) - is_asc = not self.form_data.get("order_desc") - if sort_by: - sort_by_label = utils.get_metric_name(sort_by) - if sort_by_label not in utils.get_metric_names(query_obj["metrics"]): - query_obj["metrics"].append(sort_by) - query_obj["orderby"] = [(sort_by, is_asc)] - return query_obj - - @deprecated(deprecated_in="3.0") - def to_series( # pylint: disable=too-many-branches # noqa: C901 - self, df: pd.DataFrame, classed: str = "", title_suffix: str = "" - ) -> list[dict[str, Any]]: - cols = [] - for col in df.columns: - if col == "": - cols.append("N/A") - elif col is None: - cols.append("NULL") - else: - cols.append(col) - df.columns = cols - series = df.to_dict("series") - - chart_data = [] - for name in df.T.index.tolist(): - ys = series[name] - if df[name].dtype.kind not in "biufc": - continue - series_title: list[str] | str | tuple[str, ...] - if isinstance(name, list): - series_title = [str(title) for title in name] - elif isinstance(name, tuple): - series_title = tuple(str(title) for title in name) - else: - series_title = str(name) - if ( - isinstance(series_title, (list, tuple)) - and len(series_title) > 1 - and len(self.metric_labels) == 1 - ): - # Removing metric from series name if only one metric - series_title = series_title[1:] - if title_suffix: - if isinstance(series_title, str): - series_title = (series_title, title_suffix) - elif isinstance(series_title, list): - series_title = series_title + [title_suffix] - elif isinstance(series_title, tuple): - series_title = series_title + (title_suffix,) - - values = [] - non_nan_cnt = 0 - for ds in df.index: - if ds in ys: - data = {"x": ds, "y": ys[ds]} - if not np.isnan(ys[ds]): - non_nan_cnt += 1 - else: - data = {} - values.append(data) - - if non_nan_cnt == 0: - continue - - data = {"key": series_title, "values": values} - if classed: - data["classed"] = classed - chart_data.append(data) - return chart_data - - @deprecated(deprecated_in="3.0") - def process_data(self, df: pd.DataFrame, aggregate: bool = False) -> VizData: - if df.empty: - return df - - if aggregate: - df = df.pivot_table( - index=DTTM_ALIAS, - columns=get_column_names(self.form_data.get("groupby")), - values=self.metric_labels, - fill_value=0, - aggfunc=sum, - ) - else: - df = df.pivot_table( - index=DTTM_ALIAS, - columns=get_column_names(self.form_data.get("groupby")), - values=self.metric_labels, - fill_value=self.pivot_fill_value, - ) - - rule = self.form_data.get("resample_rule") - method = self.form_data.get("resample_method") - - if rule and method: - # ``method`` comes straight from ``form_data`` and may be a - # non-string (e.g. a list) for malformed requests; guard the - # membership test so unsupported input returns a controlled - # validation error instead of an unhashable-type ``TypeError``. - if not isinstance(method, str) or method not in ALLOWED_RESAMPLE_METHODS: - raise QueryObjectValidationError( - _( - "Resample method '%(method)s' is not supported.", - method=method, - ) - ) - df = getattr(df.resample(rule), method)() - - if self.sort_series: - dfs = df.sum() - dfs.sort_values(ascending=False, inplace=True) - df = df[dfs.index] - - df = self.apply_rolling(df) - if self.form_data.get("contribution"): - dft = df.T - df = (dft / dft.sum()).T - - return df - - @deprecated(deprecated_in="3.0") - def run_extra_queries(self) -> None: - time_compare = self.form_data.get("time_compare") or [] - # backwards compatibility - if not isinstance(time_compare, list): - time_compare = [time_compare] - - max_time_compare = current_app.config["VIZ_TIME_COMPARE_MAX"] - if len(time_compare) > max_time_compare: - raise QueryObjectValidationError( - _( - "Too many time comparisons requested. The maximum allowed is " - "%(max)s, but %(count)s were requested.", - max=max_time_compare, - count=len(time_compare), - ) - ) - - for option in time_compare: - query_object = self.query_obj() - try: - delta = parse_past_timedelta(option) - except ValueError as ex: - raise QueryObjectValidationError(str(ex)) from ex - query_object["inner_from_dttm"] = query_object["from_dttm"] - query_object["inner_to_dttm"] = query_object["to_dttm"] - - if not query_object["from_dttm"] or not query_object["to_dttm"]: - raise QueryObjectValidationError( - _( - "An enclosed time range (both start and end) must be specified " - "when using a Time Comparison." - ) - ) - query_object["from_dttm"] -= delta - query_object["to_dttm"] -= delta - - df2 = self.get_df_payload(query_object, time_compare=option).get("df") - if df2 is not None and DTTM_ALIAS in df2: - dttm_series = df2[DTTM_ALIAS] + delta - df2 = df2.drop(DTTM_ALIAS, axis=1) - df2 = pd.concat([dttm_series, df2], axis=1) - label = f"{option} offset" - df2 = self.process_data(df2) - self._extra_chart_data.append((label, df2)) - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - comparison_type = self.form_data.get("comparison_type") or "values" - df = self.process_data(df) - if comparison_type == "values": - # Filter out series with all NaN - chart_data = self.to_series(df.dropna(axis=1, how="all")) - - for i, (label, df2) in enumerate(self._extra_chart_data): - chart_data.extend( - self.to_series(df2, classed=f"time-shift-{i}", title_suffix=label) - ) - else: - chart_data = [] - for i, (label, df2) in enumerate(self._extra_chart_data): - # reindex df2 into the df2 index - combined_index = df.index.union(df2.index) - df2 = ( - df2.reindex(combined_index) - .interpolate(method="time") - .reindex(df.index) - ) - - if comparison_type == "absolute": - diff = df - df2 - elif comparison_type == "percentage": - diff = (df - df2) / df2 - elif comparison_type == "ratio": - diff = df / df2 - else: - raise QueryObjectValidationError( - f"Invalid `comparison_type`: {comparison_type}" - ) - - # remove leading/trailing NaNs from the time shift difference - diff = diff[diff.first_valid_index() : diff.last_valid_index()] - - chart_data.extend( - self.to_series(diff, classed=f"time-shift-{i}", title_suffix=label) - ) - - if not self.sort_series: - chart_data = sorted(chart_data, key=lambda x: tuple(x["key"])) - return chart_data - - -class NVD3TimePivotViz(NVD3TimeSeriesViz): - """Time Series - Periodicity Pivot""" - - viz_type = "time_pivot" - sort_series = True - verbose_name = _("Time Series - Period Pivot") - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["metrics"] = [self.form_data.get("metric")] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - df = self.process_data(df) - freq = to_offset(self.form_data.get("freq")) - try: - freq = type(freq)(freq.n, normalize=True, **freq.kwds) - except ValueError: - freq = type(freq)(freq.n, **freq.kwds) - df.index.name = None - df[DTTM_ALIAS] = df.index.map(freq.rollback) - df["ranked"] = df[DTTM_ALIAS].rank(method="dense", ascending=False) - 1 - df.ranked = df.ranked.map(int) - df["series"] = "-" + df.ranked.map(str) - df["series"] = df["series"].str.replace("-0", "current") - rank_lookup = { - row["series"]: row["ranked"] for row in df.to_dict(orient="records") - } - max_ts = df[DTTM_ALIAS].max() - max_rank = df["ranked"].max() - df[DTTM_ALIAS] = df.index + (max_ts - df[DTTM_ALIAS]) - df = df.pivot_table( - index=DTTM_ALIAS, - columns="series", - values=utils.get_metric_name(self.form_data["metric"]), - ) - chart_data = self.to_series(df) - for series in chart_data: - series["rank"] = rank_lookup[series["key"]] - series["perc"] = 1 - (series["rank"] / (max_rank + 1)) - return chart_data - - -class ChordViz(BaseViz): - """A Chord diagram""" - - viz_type = "chord" - verbose_name = _("Directed Force Layout") - credits = 'Bostock' - is_timeseries = False - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["groupby"] = [ - self.form_data.get("groupby"), - self.form_data.get("columns"), - ] - query_obj["metrics"] = [self.form_data.get("metric")] - if self.form_data.get("sort_by_metric", False): - query_obj["orderby"] = [(query_obj["metrics"][0], False)] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - df.columns = ["source", "target", "value"] - - # Preparing a symmetrical matrix like d3.chords calls for - nodes = list(set(df["source"]) | set(df["target"])) - matrix = {} - for source, target in product(nodes, nodes): - matrix[(source, target)] = 0 - for source, target, value in df.to_records(index=False): - matrix[(source, target)] = value - return { - "nodes": list(nodes), - "matrix": [[matrix[(n1, n2)] for n1 in nodes] for n2 in nodes], - } - - -class CountryMapViz(BaseViz): - """A country centric""" - - viz_type = "country_map" - verbose_name = _("Country Map") - is_timeseries = False - credits = "From bl.ocks.org By john-guerra" - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - metric = self.form_data.get("metric") - entity = self.form_data.get("entity") - if not self.form_data.get("select_country"): - raise QueryObjectValidationError("Must specify a country") - if not metric: - raise QueryObjectValidationError("Must specify a metric") - if not entity: - raise QueryObjectValidationError("Must provide ISO codes") - query_obj["metrics"] = [metric] - query_obj["groupby"] = [entity] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - cols = get_column_names([self.form_data.get("entity")]) # type: ignore - metric = self.metric_labels[0] - cols += [metric] - ndf = df[cols] - df = ndf - df.columns = ["country_id", "metric"] - return df.to_dict(orient="records") - - -class WorldMapViz(BaseViz): - """A country centric world map""" - - viz_type = "world_map" - verbose_name = _("World Map") - is_timeseries = False - credits = 'datamaps on npm' - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["groupby"] = [self.form_data["entity"]] - if self.form_data.get("sort_by_metric", False): - query_obj["orderby"] = [(query_obj["metrics"][0], False)] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - # pylint: disable=import-outside-toplevel - from superset.examples import countries - - cols = get_column_names([self.form_data.get("entity")]) # type: ignore - metric = utils.get_metric_name(self.form_data["metric"]) - secondary_metric = ( - utils.get_metric_name(self.form_data["secondary_metric"]) - if self.form_data.get("secondary_metric") - else None - ) - columns = ["country", "m1", "m2"] - if metric == secondary_metric: - ndf = df[cols] - ndf["m1"] = df[metric] - ndf["m2"] = ndf["m1"] - else: - if secondary_metric: - cols += [metric, secondary_metric] - else: - cols += [metric] - columns = ["country", "m1"] - ndf = df[cols] - df = ndf - df.columns = columns - data = df.to_dict(orient="records") - for row in data: - country = None - if isinstance(row["country"], str): - if "country_fieldtype" in self.form_data: - country = countries.get( - self.form_data["country_fieldtype"], row["country"] - ) - if country: - row["code"] = country[self.form_data["country_fieldtype"]] - row["country"] = country["cca3"] - row["latitude"] = country["lat"] - row["longitude"] = country["lng"] - row["name"] = country["name"] - else: - row["country"] = "XXX" - return data - - -class ParallelCoordinatesViz(BaseViz): - """Interactive parallel coordinate implementation - - Uses this amazing javascript library - https://github.com/syntagmatic/parallel-coordinates - """ - - viz_type = "para" - verbose_name = _("Parallel Coordinates") - credits = ( - '' - "Syntagmatic's library" - ) - is_timeseries = False - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["groupby"] = [self.form_data.get("series")] - if sort_by := self.form_data.get("timeseries_limit_metric"): - sort_by_label = utils.get_metric_name(sort_by) - if sort_by_label not in utils.get_metric_names(query_obj["metrics"]): - query_obj["metrics"].append(sort_by) - if self.form_data.get("order_desc"): - query_obj["orderby"] = [ - (sort_by, not self.form_data.get("order_desc", True)) - ] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - return df.to_dict(orient="records") - - -class HorizonViz(NVD3TimeSeriesViz): - """Horizon chart - - https://www.npmjs.com/package/d3-horizon-chart - """ - - viz_type = "horizon" - verbose_name = _("Horizon Charts") - credits = ( - 'd3-horizon-chart' - ) - - -class DeckGLMultiLayer(BaseViz): - """Pile on multiple DeckGL layers""" - - viz_type = "deck_multi" - verbose_name = _("Deck.gl - Multiple Layers") - - is_timeseries = False - credits = 'deck.gl' - - @staticmethod - def _merge_filter_metadata( - *filter_groups: list[dict[str, Any]] | None, - ) -> list[dict[str, Any]]: - """Merge multiple filter metadata lists, de-duplicating identical entries. - - Used to combine the applied/rejected filter metadata reported by each - child layer into a single list for the multi-layer chart payload. - """ - merged_filters: list[dict[str, Any]] = [] - seen_filters: set[str] = set() - - for filters in filter_groups: - for filter_metadata in filters or []: - if not isinstance(filter_metadata, dict): - continue - - cache_key = json.dumps(filter_metadata, sort_keys=True) - if cache_key in seen_filters: - continue - - merged_filters.append(filter_metadata) - seen_filters.add(cache_key) - - return merged_filters - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - return {} - - def _filter_items_by_scope( - self, - items: list[Any], - layer_index: int, - layer_filter_scope: dict[str, list[int]], - ) -> list[Any]: - """Filter items based on layer filter scope.""" - filtered_items = [] - for filter_item in items: - filter_id = getattr(filter_item, "filterId", None) - if filter_id: - filter_scope = layer_filter_scope.get(filter_id, []) - if filter_scope is None: - filter_scope = [] - if not filter_scope or layer_index in filter_scope: - filtered_items.append(filter_item) - else: - filtered_items.append(filter_item) - return filtered_items - - def _process_extra_form_data_filters( - self, - layer_index: int, - layer_filter_scope: dict[str, list[int]], - filter_data_mapping: dict[str, list[Any]], - extra_form_data: dict[str, Any], - ) -> dict[str, Any]: - """Process extra_form_data filters with layer-specific filtering.""" - if not extra_form_data or not filter_data_mapping: - return extra_form_data - - filtered_extra_form_data_filters = [] - for filter_id, filter_scope in layer_filter_scope.items(): - if filter_scope is None: - filter_scope = [] - - if not filter_scope or layer_index in filter_scope: - filters_from_this_filter = filter_data_mapping.get(filter_id, []) - filtered_extra_form_data_filters.extend(filters_from_this_filter) - - return { - **extra_form_data, - "filters": filtered_extra_form_data_filters, - } - - def _apply_layer_filtering( - self, form_data: dict[str, Any], layer_index: int - ) -> dict[str, Any]: - """Apply layer-specific filtering to form data.""" - layer_filter_scope = self.form_data.get("layer_filter_scope", {}) - filter_data_mapping = self.form_data.get("filter_data_mapping", {}) - - if not layer_filter_scope: - form_data["extra_filters"] = self.form_data.get("extra_filters", []) - form_data["adhoc_filters"] = self.form_data.get("adhoc_filters") - form_data["extra_form_data"] = self.form_data.get("extra_form_data") - return form_data - - filtered_extra_filters = self._filter_items_by_scope( - self.form_data.get("extra_filters", []), layer_index, layer_filter_scope - ) - filtered_adhoc_filters = self._filter_items_by_scope( - self.form_data.get("adhoc_filters", []), layer_index, layer_filter_scope - ) - - extra_form_data = self.form_data.get("extra_form_data", {}) - filtered_extra_form_data = self._process_extra_form_data_filters( - layer_index, layer_filter_scope, filter_data_mapping, extra_form_data - ) - - form_data["extra_filters"] = filtered_extra_filters - form_data["adhoc_filters"] = filtered_adhoc_filters - form_data["extra_form_data"] = filtered_extra_form_data - return form_data - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - # Late imports to avoid circular import issues - # pylint: disable=import-outside-toplevel - from superset import db - from superset.models.slice import Slice - - slice_ids = self.form_data.get("deck_slices") or [] - max_slices = current_app.config["DECK_MULTI_MAX_SLICES"] - if len(slice_ids) > max_slices: - raise QueryObjectValidationError( - _( - "Too many sub-slices requested. The maximum allowed is " - "%(max)s, but %(count)s were requested.", - max=max_slices, - count=len(slice_ids), - ) - ) - slices = db.session.query(Slice).filter(Slice.id.in_(slice_ids)).all() - - features: dict[str, list[Any]] = {} - self.applied_filters = [] - self.rejected_filters = [] - - for layer_index, slc in enumerate(slices): - form_data = slc.form_data - form_data = self._apply_layer_filtering(form_data, layer_index) - - viz_type_name = form_data.get("viz_type") - viz_class = viz_types.get(viz_type_name) - if not viz_class: - continue # skip unknown viz types - - viz_instance = viz_class(datasource=slc.datasource, form_data=form_data) - payload = viz_instance.get_payload() - if payload: - self.applied_filters = self._merge_filter_metadata( - self.applied_filters, - payload.get("applied_filters"), - ) - self.rejected_filters = self._merge_filter_metadata( - self.rejected_filters, - payload.get("rejected_filters"), - ) - - if ( - payload - and "data" in payload - and payload["data"] is not None - and "features" in payload["data"] - ): - if viz_type_name not in features: - features[viz_type_name] = [] - features[viz_type_name].extend(payload["data"]["features"]) - - return { - "features": features, - "mapboxApiKey": current_app.config["MAPBOX_API_KEY"], - "slices": [slc.data for slc in slices if slc.data is not None], - } - - @deprecated(deprecated_in="3.0") - def get_payload(self, query_obj: QueryObjectDict | None = None) -> VizPayload: - """Extend the base payload with merged child-layer filter metadata. - - The applied/rejected filter metadata collected from each sub-slice in - ``get_data`` is merged into the base payload so dashboard filter badges - reflect the filters applied across all layers. - """ - payload = super().get_payload(query_obj) - payload["applied_filters"] = self._merge_filter_metadata( - payload.get("applied_filters"), - self.applied_filters, - ) - payload["rejected_filters"] = self._merge_filter_metadata( - payload.get("rejected_filters"), - self.rejected_filters, - ) - return payload - - -class BaseDeckGLViz(BaseViz): - """Base class for deck.gl visualizations""" - - is_timeseries = False - credits = 'deck.gl' - spatial_control_keys: list[str] = [] - - def _extract_tooltip_columns(self) -> list[str]: - """Extract column names from tooltip_contents configuration.""" - tooltip_columns = [] - if tooltip_contents := self.form_data.get("tooltip_contents", []): - for item in tooltip_contents: - if isinstance(item, str): - tooltip_columns.append(item) - elif isinstance(item, dict) and item.get("item_type") == "column": - column_name = item.get("column_name") - if column_name: - tooltip_columns.append(column_name) - return tooltip_columns - - def _add_tooltip_columns_to_query( - self, query_obj: QueryObjectDict, tooltip_columns: list[str] - ) -> None: - """Add tooltip columns to the appropriate query field.""" - if not tooltip_columns: - return - - if query_obj.get("metrics"): - existing_groupby = set(query_obj.get("groupby", [])) - for col in tooltip_columns: - if col not in existing_groupby: - query_obj.setdefault("groupby", []).append(col) - else: - existing_columns = set(query_obj.get("columns", [])) - for col in tooltip_columns: - if col not in existing_columns: - query_obj.setdefault("columns", []).append(col) - - def _integrate_tooltip_columns(self, query_obj: QueryObjectDict) -> QueryObjectDict: - """Helper method to integrate tooltip columns into query object.""" - tooltip_columns = self._extract_tooltip_columns() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - def _add_tooltip_properties( - self, properties: dict[str, Any], data: dict[str, Any] - ) -> dict[str, Any]: - """Helper method to add tooltip fields to properties dictionary.""" - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - return properties - - def _get_base_properties(self, data: dict[str, Any]) -> dict[str, Any]: - """Get base properties common to most deck.gl layers.""" - return { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - } - - def _setup_metric_label(self) -> None: - """Setup metric label for the visualization.""" - self.metric_label: str | None = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - - def __init__( - self, datasource: BaseDatasource, form_data: dict[str, Any], **kwargs: Any - ) -> None: - # Apply layer-specific filtering for deck multi layer charts in edit mode - if self._should_apply_layer_filtering(form_data): - form_data = self._apply_multilayer_filtering(form_data) - super().__init__(datasource, form_data, **kwargs) - - def _should_apply_layer_filtering(self, form_data: dict[str, Any]) -> bool: - """Check if this is a deck layer that's part of a multilayer setup.""" - return ( - "slice_id" in form_data - and "adhoc_filters" in form_data - and self._has_layer_scoped_filters(form_data) - ) - - def _has_layer_scoped_filters(self, form_data: dict[str, Any]) -> bool: - """Check if any filter has layerFilterScope (indicates multilayer context).""" - for filter_item in form_data.get("adhoc_filters", []): - if ( - isinstance(filter_item, dict) - and filter_item.get("layerFilterScope") is not None - ): - return True - return False - - def _apply_multilayer_filtering(self, form_data: dict[str, Any]) -> dict[str, Any]: - """ - Filter adhoc_filters based on layer scope for this specific layer. - - In deck multi-layer charts, each individual layer should only receive: - 1. Global filters (filters without layerFilterScope) - 2. Filters specifically scoped to this layer - - This prevents over-filtering when multiple layer-scoped filters are present. - """ - slice_id = form_data.get("slice_id") - deck_slices = self._get_deck_slices_from_filters(form_data) - - if not deck_slices or slice_id not in deck_slices: - return form_data - - layer_index = deck_slices.index(slice_id) - filtered_adhoc_filters = [] - - for filter_item in form_data.get("adhoc_filters", []): - layer_scope = self._get_filter_layer_scope(filter_item) - - # Include global filters (no layer scope) or filters scoped to this layer - if layer_scope is None or layer_index in layer_scope: - filtered_adhoc_filters.append(filter_item) - - modified_form_data = form_data.copy() - modified_form_data["adhoc_filters"] = filtered_adhoc_filters - return modified_form_data - - def _get_deck_slices_from_filters( - self, form_data: dict[str, Any] - ) -> list[int] | None: - """Extract deck_slices from any filter that contains it.""" - for filter_item in form_data.get("adhoc_filters", []): - if isinstance(filter_item, dict) and "deck_slices" in filter_item: - return filter_item["deck_slices"] - return None - - def _get_filter_layer_scope(self, filter_item: Any) -> list[int] | None: - """Extract layerFilterScope from a filter item.""" - if isinstance(filter_item, dict): - return filter_item.get("layerFilterScope") - return getattr(filter_item, "layerFilterScope", None) - - @deprecated(deprecated_in="3.0") - def get_metrics(self) -> list[str]: - # pylint: disable=attribute-defined-outside-init - self.metric = self.form_data.get("size") - return [self.metric] if self.metric else [] - - @deprecated(deprecated_in="3.0") - def process_spatial_query_obj(self, key: str, group_by: list[str]) -> None: - group_by.extend(self.get_spatial_columns(key)) - - @deprecated(deprecated_in="3.0") - def get_spatial_columns(self, key: str) -> list[str]: - spatial = self.form_data.get(key) - if spatial is None: - raise ValueError(_("Bad spatial key")) - - if spatial.get("type") == "latlong": - return [spatial.get("lonCol"), spatial.get("latCol")] - - if spatial.get("type") == "delimited": - return [spatial.get("lonlatCol")] - - if spatial.get("type") == "geohash": - return [spatial.get("geohashCol")] - return [] - - @staticmethod - @deprecated(deprecated_in="3.0") - def parse_coordinates(latlong: Any) -> tuple[float, float] | None: - if not latlong: - return None - try: - point = Point(latlong) - return (point.latitude, point.longitude) - except Exception as ex: - raise SpatialException( - _("Invalid spatial point encountered: %(latlong)s", latlong=latlong) - ) from ex - - @staticmethod - @deprecated(deprecated_in="3.0") - def reverse_geohash_decode(geohash_code: str) -> tuple[str, str]: - lat, lng = pygeohash.decode(geohash_code) - return (lng, lat) - - @staticmethod - @deprecated(deprecated_in="3.0") - def reverse_latlong(df: pd.DataFrame, key: str) -> None: - df[key] = [tuple(reversed(o)) for o in df[key] if isinstance(o, (list, tuple))] - - @deprecated(deprecated_in="3.0") - def process_spatial_data_obj(self, key: str, df: pd.DataFrame) -> pd.DataFrame: - spatial = self.form_data.get(key) - if spatial is None: - raise ValueError(_("Bad spatial key")) - - if spatial.get("type") == "latlong": - df[key] = list( - zip( - pd.to_numeric(df[spatial.get("lonCol")], errors="coerce"), - pd.to_numeric(df[spatial.get("latCol")], errors="coerce"), - strict=False, - ) - ) - elif spatial.get("type") == "delimited": - lon_lat_col = spatial.get("lonlatCol") - df[key] = df[lon_lat_col].apply(self.parse_coordinates) - del df[lon_lat_col] - elif spatial.get("type") == "geohash": - df[key] = df[spatial.get("geohashCol")].map(self.reverse_geohash_decode) - del df[spatial.get("geohashCol")] - - if spatial.get("reverseCheckbox"): - self.reverse_latlong(df, key) - - if df.get(key) is None: - raise NullValueException( - _( - "Encountered invalid NULL spatial entry, \ - please consider filtering those out" - ) - ) - return df - - @deprecated(deprecated_in="3.0") - def add_null_filters(self) -> None: - spatial_columns = set() - for key in self.spatial_control_keys: - for column in self.get_spatial_columns(key): - spatial_columns.add(column) - - if self.form_data.get("adhoc_filters") is None: - self.form_data["adhoc_filters"] = [] - - if line_column := self.form_data.get("line_column"): - spatial_columns.add(line_column) - - for column in sorted(spatial_columns): - filter_ = simple_filter_to_adhoc( - {"col": column, "op": "IS NOT NULL", "val": ""} - ) - self.form_data["adhoc_filters"].append(filter_) - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - # add NULL filters - if self.form_data.get("filter_nulls", True): - self.add_null_filters() - - query_obj = super().query_obj() - group_by: list[str] = [] - - for key in self.spatial_control_keys: - self.process_spatial_query_obj(key, group_by) - - if self.form_data.get("dimension"): - group_by += [self.form_data["dimension"]] - - if self.form_data.get("js_columns"): - group_by += self.form_data.get("js_columns") or [] - # 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. - group_by = sorted(set(group_by)) - if metrics := self.get_metrics(): - query_obj["groupby"] = group_by - query_obj["metrics"] = metrics - query_obj["columns"] = [] - first_metric = query_obj["metrics"][0] - query_obj["orderby"] = [ - (first_metric, not self.form_data.get("order_desc", True)) - ] - else: - query_obj["columns"] = group_by - return query_obj - - @deprecated(deprecated_in="3.0") - def get_js_columns(self, data: dict[str, Any]) -> dict[str, Any]: - cols = self.form_data.get("js_columns") or [] - return {col: data.get(col) for col in cols} - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - # Processing spatial info - for key in self.spatial_control_keys: - df = self.process_spatial_data_obj(key, df) - - features = [] - for data in df.to_dict(orient="records"): - feature = self.get_properties(data) - extra_props = self.get_js_columns(data) - if extra_props: - feature["extraProps"] = extra_props - features.append(feature) - - return { - "features": features, - "mapboxApiKey": current_app.config["MAPBOX_API_KEY"], - "metricLabels": self.metric_labels, - } - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - raise NotImplementedError() - - -class DeckScatterViz(BaseDeckGLViz): - """deck.gl's ScatterLayer""" - - viz_type = "deck_scatter" - verbose_name = _("Deck.gl - Scatter plot") - spatial_control_keys = ["spatial"] - is_timeseries = True - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - # pylint: disable=attribute-defined-outside-init - self.is_timeseries = bool(self.form_data.get("time_grain_sqla")) - self.point_radius_fixed = self.form_data.get("point_radius_fixed") or { - "type": "fix", - "value": 500, - } - - query_obj = super().query_obj() - return self._integrate_tooltip_columns(query_obj) - - @deprecated(deprecated_in="3.0") - def get_metrics(self) -> list[str]: - # pylint: disable=attribute-defined-outside-init - self.metric = None - if self.point_radius_fixed.get("type") == "metric": - self.metric = self.point_radius_fixed["value"] - return [self.metric] - return [] - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "metric": data.get(self.metric_label) if self.metric_label else None, - "radius": ( - self.fixed_value - if self.fixed_value - else data.get(self.metric_label) - if self.metric_label - else None - ), - "cat_color": data.get(self.dim) if self.dim else None, - "position": data.get("spatial"), - DTTM_ALIAS: data.get(DTTM_ALIAS), - } - - return self._add_tooltip_properties(properties, data) - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self._setup_metric_label() - # pylint: disable=attribute-defined-outside-init - self.point_radius_fixed = self.form_data.get("point_radius_fixed") - self.fixed_value = None - self.dim = self.form_data.get("dimension") - if self.point_radius_fixed and self.point_radius_fixed.get("type") != "metric": - self.fixed_value = self.point_radius_fixed.get("value") - return super().get_data(df) - - -class DeckScreengrid(BaseDeckGLViz): - """deck.gl's ScreenGridLayer""" - - viz_type = "deck_screengrid" - verbose_name = _("Deck.gl - Screen Grid") - spatial_control_keys = ["spatial"] - is_timeseries = True - - def _extract_tooltip_columns(self) -> list[str]: - """Extract column names from tooltip_contents configuration.""" - tooltip_columns = [] - if tooltip_contents := self.form_data.get("tooltip_contents", []): - for item in tooltip_contents: - if isinstance(item, str): - tooltip_columns.append(item) - elif isinstance(item, dict) and item.get("item_type") == "column": - column_name = item.get("column_name") - if column_name: - tooltip_columns.append(column_name) - return tooltip_columns - - def _add_tooltip_columns_to_query( - self, query_obj: QueryObjectDict, tooltip_columns: list[str] - ) -> None: - """Add tooltip columns to the appropriate query field.""" - if not tooltip_columns: - return - - if query_obj.get("metrics"): - # Add to groupby when metrics exist - existing_groupby = set(query_obj.get("groupby", [])) - for col in tooltip_columns: - if col not in existing_groupby: - query_obj.setdefault("groupby", []).append(col) - else: - # Add to columns when no metrics - existing_columns = set(query_obj.get("columns", [])) - for col in tooltip_columns: - if col not in existing_columns: - query_obj.setdefault("columns", []).append(col) - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - self.is_timeseries = bool(self.form_data.get("time_grain_sqla")) - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - "__timestamp": data.get(DTTM_ALIAS) or data.get("__time"), - } - - # For Screengrid, the frontend handles the cell-to-points mapping - # The backend just provides the aggregated data and tooltip fields - - # For Screengrid, automatically collect all values for tooltip fields - # When user selects LON as tooltip field, {{ LON }} will show all LON values - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - # For Screengrid, each cell contains multiple points, provide as-is - # The frontend will handle displaying multiple values appropriately - properties[col] = data[col] - - return properties - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -class DeckGrid(BaseDeckGLViz): - """deck.gl's GridLayer""" - - viz_type = "deck_grid" - verbose_name = _("Deck.gl - 3D Grid") - spatial_control_keys = ["spatial"] - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - } - # Add tooltip fields to properties - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - return properties - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -@deprecated(deprecated_in="3.0") -def geohash_to_json(geohash_code: str) -> list[list[float]]: - # Get the center and the error margins - lat, lon, lat_err, lon_err = pygeohash.decode_exactly(geohash_code) - # Calculate the Bounding Box - bbox = { - "n": lat + lat_err, - "s": lat - lat_err, - "e": lon + lon_err, - "w": lon - lon_err, - } - return [ - [bbox["w"], bbox["n"]], - [bbox["e"], bbox["n"]], - [bbox["e"], bbox["s"]], - [bbox["w"], bbox["s"]], - [bbox["w"], bbox["n"]], - ] - - -class DeckPathViz(BaseDeckGLViz): - """deck.gl's PathLayer""" - - viz_type = "deck_path" - verbose_name = _("Deck.gl - Paths") - deck_viz_key = "path" - is_timeseries = True - deser_map = { - "json": json.loads, - "polyline": polyline.decode, - "geohash": geohash_to_json, - } - - def _extract_tooltip_columns(self) -> list[str]: - tooltip_columns = [] - if tooltip_contents := self.form_data.get("tooltip_contents", []): - for item in tooltip_contents: - if isinstance(item, str): - tooltip_columns.append(item) - elif isinstance(item, dict) and item.get("item_type") == "column": - column_name = item.get("column_name") - if column_name: - tooltip_columns.append(column_name) - return tooltip_columns - - def _add_tooltip_columns_to_query( - self, query_obj: QueryObjectDict, tooltip_columns: list[str] - ) -> None: - if not tooltip_columns: - return - if query_obj.get("metrics"): - existing_groupby = set(query_obj.get("groupby", [])) - for col in tooltip_columns: - if col not in existing_groupby: - query_obj.setdefault("groupby", []).append(col) - else: - existing_columns = set(query_obj.get("columns", [])) - for col in tooltip_columns: - if col not in existing_columns: - query_obj.setdefault("columns", []).append(col) - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - # pylint: disable=attribute-defined-outside-init - self.is_timeseries = bool(self.form_data.get("time_grain_sqla")) - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self.metric = self.form_data.get("metric") - line_col = self.form_data.get("line_column") - if query_obj["metrics"]: - self.has_metrics = True - query_obj["groupby"].append(line_col) - else: - self.has_metrics = False - query_obj["columns"].append(line_col) - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - line_type = self.form_data["line_type"] - deser = self.deser_map[line_type] - line_column = self.form_data["line_column"] - path = deser(data[line_column]) - if self.form_data.get("reverse_long_lat"): - path = [(o[1], o[0]) for o in path] - data[self.deck_viz_key] = path - data["__timestamp"] = data.get(DTTM_ALIAS) or data.get("__time") - return data - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -class DeckPolygon(DeckPathViz): - """deck.gl's Polygon Layer""" - - viz_type = "deck_polygon" - deck_viz_key = "polygon" - verbose_name = _("Deck.gl - Polygon") - - def _extract_tooltip_columns(self) -> list[str]: - tooltip_columns = [] - if tooltip_contents := self.form_data.get("tooltip_contents", []): - for item in tooltip_contents: - if isinstance(item, str): - tooltip_columns.append(item) - elif isinstance(item, dict) and item.get("item_type") == "column": - column_name = item.get("column_name") - if column_name: - tooltip_columns.append(column_name) - return tooltip_columns - - def _add_tooltip_columns_to_query( - self, query_obj: QueryObjectDict, tooltip_columns: list[str] - ) -> None: - if not tooltip_columns: - return - if query_obj.get("metrics"): - existing_groupby = set(query_obj.get("groupby", [])) - for col in tooltip_columns: - if col not in existing_groupby: - query_obj.setdefault("groupby", []).append(col) - else: - existing_columns = set(query_obj.get("columns", [])) - for col in tooltip_columns: - if col not in existing_columns: - query_obj.setdefault("columns", []).append(col) - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - self.elevation = self.form_data.get("point_radius_fixed") or { - "type": "fix", - "value": 500, - } - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_metrics(self) -> list[str]: - metrics = [self.form_data.get("metric")] - if self.elevation.get("type") == "metric": - metrics.append(self.elevation.get("value")) - return [metric for metric in metrics if metric] - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - super().get_properties(data) - elevation = self.form_data["point_radius_fixed"]["value"] - type_ = self.form_data["point_radius_fixed"]["type"] - data["elevation"] = ( - data.get(utils.get_metric_name(elevation)) - if type_ == "metric" - else elevation - ) - return data - - -class DeckHex(BaseDeckGLViz): - """deck.gl's DeckLayer""" - - viz_type = "deck_hex" - verbose_name = _("Deck.gl - 3D HEX") - spatial_control_keys = ["spatial"] - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - } - # Add tooltip fields to properties - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - return properties - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -class DeckHeatmap(BaseDeckGLViz): - """deck.gl's HeatmapLayer""" - - viz_type = "deck_heatmap" - verbose_name = _("Deck.gl - Heatmap") - spatial_control_keys = ["spatial"] - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - } - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - - return properties - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -class DeckContour(BaseDeckGLViz): - """deck.gl's ContourLayer""" - - viz_type = "deck_contour" - verbose_name = _("Deck.gl - Contour") - spatial_control_keys = ["spatial"] - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - - # Extract and add tooltip columns - tooltip_columns = self._extract_tooltip_columns() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - - return query_obj - - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - properties = { - "position": data.get("spatial"), - "weight": (data.get(self.metric_label) if self.metric_label else None) or 1, - # Include all original data for tooltip access - "_originalData": data, - } - - # Add tooltip data at top level too for backward compatibility - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - - return properties - - def get_data(self, df: pd.DataFrame) -> VizData: - self.metric_label = ( # pylint: disable=attribute-defined-outside-init - utils.get_metric_name(self.metric) if self.metric else None - ) - return super().get_data(df) - - -class DeckGeoJson(BaseDeckGLViz): - """deck.gl's GeoJSONLayer""" - - viz_type = "deck_geojson" - verbose_name = _("Deck.gl - GeoJSON") - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - query_obj["columns"] += [self.form_data.get("geojson")] - query_obj["metrics"] = [] - query_obj["groupby"] = [] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - geojson = data[get_column_name(self.form_data["geojson"])] - return json.loads(geojson) - - -class DeckArc(BaseDeckGLViz): - """deck.gl's Arc Layer""" - - viz_type = "deck_arc" - verbose_name = _("Deck.gl - Arc") - spatial_control_keys = ["start_spatial", "end_spatial"] - is_timeseries = True - - def _extract_tooltip_columns(self) -> list[str]: - tooltip_columns = [] - if tooltip_contents := self.form_data.get("tooltip_contents", []): - for item in tooltip_contents: - if isinstance(item, str): - tooltip_columns.append(item) - elif isinstance(item, dict) and item.get("item_type") == "column": - column_name = item.get("column_name") - if column_name: - tooltip_columns.append(column_name) - return tooltip_columns - - def _add_tooltip_columns_to_query( - self, query_obj: QueryObjectDict, tooltip_columns: list[str] - ) -> None: - if not tooltip_columns: - return - if query_obj.get("metrics"): - existing_groupby = set(query_obj.get("groupby", [])) - for col in tooltip_columns: - if col not in existing_groupby: - query_obj.setdefault("groupby", []).append(col) - else: - existing_columns = set(query_obj.get("columns", [])) - for col in tooltip_columns: - if col not in existing_columns: - query_obj.setdefault("columns", []).append(col) - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - self.is_timeseries = bool(self.form_data.get("time_grain_sqla")) - tooltip_columns = self._extract_tooltip_columns() - query_obj = super().query_obj() - self._add_tooltip_columns_to_query(query_obj, tooltip_columns) - return query_obj - - @deprecated(deprecated_in="3.0") - def get_properties(self, data: dict[str, Any]) -> dict[str, Any]: - dim = self.form_data.get("dimension") - properties = { - "sourcePosition": data.get("start_spatial"), - "targetPosition": data.get("end_spatial"), - "cat_color": data.get(dim) if dim else None, - DTTM_ALIAS: data.get(DTTM_ALIAS), - } - # Add tooltip fields to properties - tooltip_columns = self._extract_tooltip_columns() - for col in tooltip_columns: - if col in data: - properties[col] = data[col] - return properties - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - return { - "features": super().get_data(df)["features"], - "mapboxApiKey": current_app.config["MAPBOX_API_KEY"], - } - - -class PairedTTestViz(BaseViz): - """A table displaying paired t-test values""" - - viz_type = "paired_ttest" - verbose_name = _("Time Series - Paired t-test") - sort_series = False - is_timeseries = True - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - if sort_by := self.form_data.get("timeseries_limit_metric"): - sort_by_label = utils.get_metric_name(sort_by) - if sort_by_label not in utils.get_metric_names(query_obj["metrics"]): - query_obj["metrics"].append(sort_by) - if self.form_data.get("order_desc"): - query_obj["orderby"] = [ - (sort_by, not self.form_data.get("order_desc", True)) - ] - return query_obj - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - """ - Transform received data frame into an object of the form: - { - 'metric1': [ - { - groups: ('groupA', ... ), - values: [ {x, y}, ... ], - }, ... - ], ... - } - """ - - if df.empty: - return None - - groups = get_column_names(self.form_data.get("groupby")) - metrics = self.metric_labels - df = df.pivot_table(index=DTTM_ALIAS, columns=groups, values=metrics) - cols = [] - # Be rid of falsy keys - for col in df.columns: - if col == "": - cols.append("N/A") - elif col is None: - cols.append("NULL") - else: - cols.append(col) - df.columns = cols - data: dict[str, list[dict[str, Any]]] = {} - series = df.to_dict("series") - for name_set in df.columns: - # If no groups are defined, nameSet will be the metric name - has_group = not isinstance(name_set, str) - data_ = { - "group": name_set[1:] if has_group else "All", - "values": [ - { - "x": t, - "y": series[name_set][t] if t in series[name_set] else None, - } - for t in df.index - ], - } - key = name_set[0] if has_group else name_set - if key in data: - data[key].append(data_) - else: - data[key] = [data_] - return data - - -class RoseViz(NVD3TimeSeriesViz): - viz_type = "rose" - verbose_name = _("Time Series - Nightingale Rose Chart") - sort_series = False - is_timeseries = True - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - - data = super().get_data(df) - result: dict[str, list[dict[str, str]]] = {} - for datum in data: - key = datum["key"] - for val in datum["values"]: - timestamp = val["x"].value - if not result.get(timestamp): - result[timestamp] = [] - value = 0 if math.isnan(val["y"]) else val["y"] - result[timestamp].append( - { - "key": key, - "value": value, # type: ignore - "name": ", ".join(key) if isinstance(key, list) else key, - "time": val["x"], - } - ) - return result - - -class PartitionViz(NVD3TimeSeriesViz): - """ - A hierarchical data visualization with support for time series. - """ - - viz_type = "partition" - verbose_name = _("Partition Diagram") - - @deprecated(deprecated_in="3.0") - def query_obj(self) -> QueryObjectDict: - query_obj = super().query_obj() - time_op = self.form_data.get("time_series_option", "not_time") - # Return time series data if the user specifies so - query_obj["is_timeseries"] = time_op != "not_time" - return query_obj - - @staticmethod - @deprecated(deprecated_in="3.0") - def levels_for( - time_op: str, groups: list[str], df: pd.DataFrame - ) -> dict[int, pd.Series]: - """ - Compute the partition at each `level` from the dataframe. - """ - levels = {} - for i in range(0, len(groups) + 1): - agg_df = df.groupby(groups[:i]) if i else df - levels[i] = ( - agg_df.mean(numeric_only=True) - if time_op == "agg_mean" - else agg_df.sum(numeric_only=True) - ) - return levels - - @staticmethod - @deprecated(deprecated_in="3.0") - def levels_for_diff( - time_op: str, groups: list[str], df: pd.DataFrame - ) -> dict[int, pd.DataFrame]: - # Obtain a unique list of the time grains - times = list(set(df[DTTM_ALIAS])) - times.sort() - until = times[len(times) - 1] - since = times[0] - # Function describing how to calculate the difference - func = { - "point_diff": [pd.Series.sub, lambda a, b, fill_value: a - b], - "point_factor": [pd.Series.div, lambda a, b, fill_value: a / float(b)], - "point_percent": [ - lambda a, b, fill_value=0: a.div(b, fill_value=fill_value) - 1, - lambda a, b, fill_value: a / float(b) - 1, - ], - }[time_op] - agg_df = df.groupby(DTTM_ALIAS).sum(numeric_only=True) - levels = { - 0: pd.Series( - { - m: func[1](agg_df[m][until], agg_df[m][since], 0) - for m in agg_df.columns - } - ) - } - for i in range(1, len(groups) + 1): - agg_df = df.groupby([DTTM_ALIAS] + groups[:i]).sum(numeric_only=True) - levels[i] = pd.DataFrame( - { - m: func[0](agg_df[m][until], agg_df[m][since], fill_value=0) - for m in agg_df.columns - } - ) - return levels - - @deprecated(deprecated_in="3.0") - def levels_for_time( - self, groups: list[str], df: pd.DataFrame - ) -> dict[int, VizData]: - procs = {} - for i in range(0, len(groups) + 1): - self.form_data["groupby"] = groups[:i] - df_drop = df.drop(groups[i:], axis=1) - procs[i] = self.process_data(df_drop, aggregate=True) - self.form_data["groupby"] = groups - return procs - - @deprecated(deprecated_in="3.0") - def nest_values( - self, - levels: dict[int, pd.DataFrame], - level: int = 0, - metric: str | None = None, - dims: list[str] | None = None, - ) -> list[dict[str, Any]]: - """ - Nest values at each level on the back-end with - access and setting, instead of summing from the bottom. - """ - if dims is None: - dims = [] - if not level: - return [ - { - "name": m, - "val": levels[0][m], - "children": self.nest_values(levels, 1, m), - } - for m in levels[0].index - ] - if level == 1: - metric_level = levels[1][metric] - return [ - { - "name": i, - "val": metric_level[i], - "children": self.nest_values(levels, 2, metric, [i]), - } - for i in metric_level.index - ] - if level >= len(levels): - return [] - - dim_level = levels[level][metric] - for d in dims: - if d not in dim_level: - return [] - dim_level = dim_level[d] - - return [ - { - "name": [*dims, i], - "val": dim_level[i], - "children": self.nest_values(levels, level + 1, metric, dims + [i]), - } - for i in dim_level.index - ] - - @deprecated(deprecated_in="3.0") - def nest_procs( - self, - procs: dict[int, pd.DataFrame], - level: int = -1, - dims: tuple[str, ...] | None = None, - time: Any = None, - ) -> list[dict[str, Any]]: - if dims is None: - dims = () - if level == -1: - return [ - {"name": m, "children": self.nest_procs(procs, 0, (m,))} - for m in procs[0].columns - ] - if not level: - return [ - { - "name": t, - "val": procs[0][dims[0]][t], - "children": self.nest_procs(procs, 1, dims, t), - } - for t in procs[0].index - ] - if level >= len(procs): - return [] - return [ - { - "name": i, - "val": procs[level][dims][i][time], - "children": self.nest_procs(procs, level + 1, dims + (i,), time), - } - for i in procs[level][dims].columns - ] - - @deprecated(deprecated_in="3.0") - def get_data(self, df: pd.DataFrame) -> VizData: - if df.empty: - return None - groups = get_column_names(self.form_data.get("groupby")) - time_op = self.form_data.get("time_series_option", "not_time") - if not groups: - raise ValueError(_("Please choose at least one groupby")) - if time_op == "not_time": - levels = self.levels_for("agg_sum", groups, df) - elif time_op in ["agg_sum", "agg_mean"]: - levels = self.levels_for(time_op, groups, df) - elif time_op in ["point_diff", "point_factor", "point_percent"]: - levels = self.levels_for_diff(time_op, groups, df) - elif time_op == "adv_anal": - procs = self.levels_for_time(groups, df) - return self.nest_procs(procs) - else: - levels = self.levels_for("agg_sum", [DTTM_ALIAS] + groups, df) - return self.nest_values(levels) - - -def _get_subclasses(cls: type[BaseViz]) -> set[type[BaseViz]]: - return set(cls.__subclasses__()).union( - [sc for c in cls.__subclasses__() for sc in _get_subclasses(c)] - ) - - -@deprecated(deprecated_in="3.0") -def get_subclasses(cls: type[BaseViz]) -> set[type[BaseViz]]: - return _get_subclasses(cls) - - -viz_types = { - o.viz_type: o - for o in _get_subclasses(BaseViz) - if o.viz_type not in current_app.config["VIZ_TYPE_DENYLIST"] -} diff --git a/tests/integration_tests/core_tests.py b/tests/integration_tests/core_tests.py index 0a4ed98b2b2f..76b3768ee76a 100644 --- a/tests/integration_tests/core_tests.py +++ b/tests/integration_tests/core_tests.py @@ -43,7 +43,7 @@ from superset.connectors.sqla.models import SqlaTable from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.mssql import MssqlEngineSpec -from superset.exceptions import OAuth2RedirectError, SupersetException +from superset.exceptions import SupersetException from superset.extensions import cache_manager from superset.models import core as models from superset.models.dashboard import Dashboard @@ -120,29 +120,6 @@ def test_slice_endpoint(self): resp = self.client.get("/slice/-1/") assert resp.status_code == 404 - @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") - def test_viz_cache_key(self): - self.login(ADMIN_USERNAME) - slc = self.get_slice("% Rural") - - viz = slc.viz - qobj = viz.query_obj() - cache_key = viz.cache_key(qobj) - - qobj["groupby"] = [] - cache_key_with_groupby = viz.cache_key(qobj) - assert cache_key != cache_key_with_groupby - - assert viz.cache_key(qobj) != viz.cache_key(qobj, time_compare="12 weeks") - - assert viz.cache_key(qobj, time_compare="28 days") != viz.cache_key( - qobj, time_compare="12 weeks" - ) - - qobj["inner_from_dttm"] = datetime.datetime(1901, 1, 1) - - assert cache_key_with_groupby == viz.cache_key(qobj) - def test_admin_only_menu_views(self): def assert_admin_view_menus_in(role_name, assert_func): role = security_manager.find_role(role_name) @@ -446,84 +423,6 @@ def test_comments_in_sqlatable_query(self): assert "comment 2" in rendered_query assert "FROM tbl" in rendered_query - def test_slice_payload_no_datasource(self): - form_data = { - "viz_type": "dist_bar", - } - self.login(ADMIN_USERNAME) - rv = self.client.post( - "/explore_json/", - data={"form_data": json.dumps(form_data)}, - ) - data = json.loads(rv.data.decode("utf-8")) - - assert ( - data["errors"][0]["message"] - == "The dataset associated with this chart no longer exists" - ) - - def test_explore_json_data_invalid_cache_key(self): - self.login(ADMIN_USERNAME) - cache_key = "invalid-cache-key" - rv = self.client.get(f"/explore_json/data/{cache_key}") - data = json.loads(rv.data.decode("utf-8")) - - assert rv.status_code == 404 - assert data["error"] == "Cached data not found" - - @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") - @mock.patch("superset.viz.BaseViz.get_df") - def test_explore_json_propagates_oauth2_redirect_error( - self, mock_get_df: mock.Mock - ) -> None: - """ - SupersetErrorException exceptions bubble up properly. - """ - mock_get_df.side_effect = OAuth2RedirectError( - url="https://accounts.example.com/o/oauth2/v2/auth?...", - tab_id="tab-123", - redirect_uri="https://superset.example.com/oauth2/redirect", - ) - - self.login(ADMIN_USERNAME) - slc = self.get_slice("% Rural") - rv = self.client.post( - f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/", - data={"form_data": json.dumps(slc.form_data)}, - ) - data = json.loads(rv.data.decode("utf-8")) - - assert "errors" in data, data - assert data["errors"][0]["error_type"] == "OAUTH2_REDIRECT" - assert data["errors"][0]["extra"] == { - "url": "https://accounts.example.com/o/oauth2/v2/auth?...", - "tab_id": "tab-123", - "redirect_uri": "https://superset.example.com/oauth2/redirect", - } - - @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") - @mock.patch("superset.viz.BaseViz.get_df") - def test_explore_json_generic_exception_still_returns_viz_get_df_error( - self, mock_get_df: mock.Mock - ) -> None: - """ - Non-Superset exceptions raised by ``get_df`` are reported as the - generic ``VIZ_GET_DF_ERROR``. - """ - mock_get_df.side_effect = RuntimeError("boom") - - self.login(ADMIN_USERNAME) - slc = self.get_slice("% Rural") - rv = self.client.post( - f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/", - data={"form_data": json.dumps(slc.form_data)}, - ) - data = json.loads(rv.data.decode("utf-8")) - - assert "errors" in data, data - assert data["errors"][0]["error_type"] == "VIZ_GET_DF_ERROR" - assert data["errors"][0]["message"] == "boom" - def test_results_default_deserialization(self): use_new_deserialization = False data = [("a", 4, 4.0, "2019-08-18T16:39:16.660000")] diff --git a/tests/integration_tests/security/guest_token_security_tests.py b/tests/integration_tests/security/guest_token_security_tests.py index 9caf4706a033..a819947df49f 100644 --- a/tests/integration_tests/security/guest_token_security_tests.py +++ b/tests/integration_tests/security/guest_token_security_tests.py @@ -305,7 +305,7 @@ def setUp(self) -> None: def test_raise_for_access__happy_path(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: security_manager.raise_for_access( **{ kwarg: Mock( @@ -323,7 +323,7 @@ def test_raise_for_access__happy_path(self): def test_raise_for_access__native_filter_happy_path(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: security_manager.raise_for_access( **{ kwarg: Mock( @@ -343,7 +343,7 @@ def test_raise_for_access__native_filter_happy_path(self): def test_raise_for_access__no_dashboard_in_form_data(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -362,7 +362,7 @@ def test_raise_for_access__drill_to_detail_happy_path(self): the embedded user has access to. """ g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: security_manager.raise_for_access( **{ kwarg: Mock( @@ -382,7 +382,7 @@ def test_raise_for_access__drill_to_detail_datasource_not_on_dashboard(self): with the dashboard the embedded user has access to. """ g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -404,7 +404,7 @@ def test_raise_for_access__drill_by_happy_path(self): embedded user has access to. """ g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: security_manager.raise_for_access( **{ kwarg: Mock( @@ -427,7 +427,7 @@ def test_raise_for_access__drill_by_chart_not_on_dashboard(self): dashboard the embedded user has access to. """ g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -451,7 +451,7 @@ def test_raise_for_access__drill_by_columns_not_drillable(self): drillable (groupby=True) on the datasource. """ g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -471,7 +471,7 @@ def test_raise_for_access__drill_by_columns_not_drillable(self): def test_raise_for_access__chart_not_on_dashboard(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -487,7 +487,7 @@ def test_raise_for_access__chart_not_on_dashboard(self): def test_raise_for_access__chart_doesnt_belong_to_datasource(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -503,7 +503,7 @@ def test_raise_for_access__chart_doesnt_belong_to_datasource(self): def test_raise_for_access__native_filter_no_id_in_form_data(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -523,7 +523,7 @@ def test_raise_for_access__native_filter_no_id_in_form_data(self): def test_raise_for_access__native_filter_datasource_not_associated(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -548,7 +548,7 @@ def test_raise_for_access__native_filter_datasource_not_associated(self): ) def test_raise_for_access__embedded_feature_flag_off(self): g.user = self.authorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ @@ -564,7 +564,7 @@ def test_raise_for_access__embedded_feature_flag_off(self): def test_raise_for_access__unauthorized_guest_user(self): g.user = self.unauthorized_guest - for kwarg in ["viz", "query_context"]: + for kwarg in ["query_context"]: with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access( **{ diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index ad205f9d2270..a9211bb24602 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -31,7 +31,7 @@ from flask_appbuilder.security.sqla.models import Role from superset.daos.datasource import DatasourceDAO # noqa: F401 from superset.models.dashboard import Dashboard -from superset import appbuilder, db, security_manager, viz +from superset import appbuilder, db, security_manager from superset.connectors.sqla.models import SqlaTable from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException @@ -1428,8 +1428,6 @@ def assert_can_gamma(self, perm_set): assert ("can_explore", "Superset") in perm_set assert ("can_share_chart", "Superset") in perm_set assert ("can_share_dashboard", "Superset") in perm_set - assert ("can_explore_json", "Superset") in perm_set - assert ("can_explore_json", "Superset") in perm_set assert ("can_userinfo", "UserDBModelView") in perm_set assert ("can_view_chart_as_table", "Dashboard") in perm_set assert ("can_view_query", "Dashboard") in perm_set @@ -1524,9 +1522,6 @@ def test_is_public_pvm(self): assert security_manager._is_public_pvm( security_manager.find_permission_view_menu("can_dashboard", "Superset") ) - assert security_manager._is_public_pvm( - security_manager.find_permission_view_menu("can_explore_json", "Superset") - ) # Should NOT include write permissions on core objects assert not security_manager._is_public_pvm( @@ -1553,7 +1548,6 @@ def test_public_role_permissions(self): assert ("can_read", "Chart") in public_perm_set assert ("can_dashboard", "Superset") in public_perm_set assert ("can_slice", "Superset") in public_perm_set - assert ("can_explore_json", "Superset") in public_perm_set assert ("can_dashboard_permalink", "Superset") in public_perm_set # Filter state for interactive dashboards @@ -1700,7 +1694,6 @@ def test_gamma_permissions(self): assert ("can_explore", "Superset") in gamma_perm_set assert ("can_share_chart", "Superset") in gamma_perm_set assert ("can_share_dashboard", "Superset") in gamma_perm_set - assert ("can_explore_json", "Superset") in gamma_perm_set assert ("can_userinfo", "UserDBModelView") in gamma_perm_set assert ("can_view_chart_as_table", "Dashboard") in gamma_perm_set assert ("can_view_query", "Dashboard") in gamma_perm_set @@ -2134,24 +2127,6 @@ def test_raise_for_access_table(self, mock_can_access): with self.assertRaises(SupersetSecurityException): # noqa: PT027 security_manager.raise_for_access(database=database, table=table) - @patch("superset.security.SupersetSecurityManager.is_editor") - @patch("superset.security.SupersetSecurityManager.can_access") - @patch("superset.security.SupersetSecurityManager.can_access_schema") - def test_raise_for_access_viz( - self, mock_can_access_schema, mock_can_access, mock_is_editor - ): - test_viz = viz.TimeTableViz(self.get_datasource_mock(), form_data={}) - - mock_can_access_schema.return_value = True - security_manager.raise_for_access(viz=test_viz) - - mock_can_access.return_value = False - mock_can_access_schema.return_value = False - mock_is_editor.return_value = False - with override_user(security_manager.find_user("gamma")): - with self.assertRaises(SupersetSecurityException): # noqa: PT027 - security_manager.raise_for_access(viz=test_viz) - def test_get_admin_user_roles(self): admin = security_manager.find_user("admin") with override_user(admin): diff --git a/tests/integration_tests/tasks/async_queries_tests.py b/tests/integration_tests/tasks/async_queries_tests.py index 728076011bd4..16d618f0ece2 100644 --- a/tests/integration_tests/tasks/async_queries_tests.py +++ b/tests/integration_tests/tasks/async_queries_tests.py @@ -29,7 +29,6 @@ ) from superset.commands.chart.data.get_data_command import ChartDataCommand from superset.commands.chart.exceptions import ChartDataQueryFailedError -from superset.exceptions import SupersetException from superset.extensions import async_query_manager, security_manager from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.fixtures.birth_names_dashboard import ( @@ -154,120 +153,3 @@ def test_soft_timeout_load_chart_data_into_cache( set_form_data.side_effect = SoftTimeLimitExceeded() load_chart_data_into_cache(job_metadata, form_data) set_form_data.assert_called_once_with(form_data, "error", errors=errors) - - @parameterized.expand( - [ - ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)), - ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)), - ] - ) - @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices") - @pytest.mark.skip(reason="This test will be changed to use the api/v1/data") - @mock.patch.object(async_query_manager, "update_job") - def test_load_explore_json_into_cache( - self, cache_type, cache_backend, mock_update_job - ): - from superset.tasks.async_queries import load_explore_json_into_cache - - app._got_first_request = False - - async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend) - async_query_manager.init_app(app) - - table = self.get_table(name="birth_names") - user = security_manager.find_user("gamma") - form_data = { - "datasource": f"{table.id}__table", - "viz_type": "dist_bar", - "granularity_sqla": "ds", - "time_range": "No filter", - "metrics": ["count"], - "adhoc_filters": [], - "groupby": ["gender"], - "row_limit": 100, - } - job_metadata = { - "channel_id": str(uuid4()), - "job_id": str(uuid4()), - "user_id": user.id, - "status": "pending", - "errors": [], - } - - load_explore_json_into_cache(job_metadata, form_data) - - mock_update_job.assert_called_once_with( - job_metadata, "done", result_url=mock.ANY - ) - - @parameterized.expand( - [ - ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)), - ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)), - ] - ) - @mock.patch.object(async_query_manager, "update_job") - @mock.patch("superset.tasks.async_queries.set_form_data") - def test_load_explore_json_into_cache_error( - self, cache_type, cache_backend, mock_set_form_data, mock_update_job - ): - from superset.tasks.async_queries import load_explore_json_into_cache - - app._got_first_request = False - - async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend) - async_query_manager.init_app(app) - - user = security_manager.find_user("gamma") - form_data = {} - job_metadata = { - "channel_id": str(uuid4()), - "job_id": str(uuid4()), - "user_id": user.id, - "status": "pending", - "errors": [], - } - - with pytest.raises(SupersetException): - load_explore_json_into_cache(job_metadata, form_data) - - mock_set_form_data.assert_called_once_with(form_data) - errors = ["The dataset associated with this chart no longer exists"] - mock_update_job.assert_called_once_with(job_metadata, "error", errors=errors) - - @parameterized.expand( - [ - ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)), - ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)), - ] - ) - @mock.patch.object(ChartDataCommand, "run") - @mock.patch.object(async_query_manager, "update_job") - def test_soft_timeout_load_explore_json_into_cache( - self, cache_type, cache_backend, mock_update_job, mock_run_command - ): - from superset.tasks.async_queries import load_explore_json_into_cache - - app._got_first_request = False - - async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend) - async_query_manager.init_app(app) - - user = security_manager.find_user("gamma") - form_data = {} - job_metadata = { - "channel_id": str(uuid4()), - "job_id": str(uuid4()), - "user_id": user.id, - "status": "pending", - "errors": [], - } - errors = ["A timeout occurred while loading explore JSON data"] - - with pytest.raises(SoftTimeLimitExceeded): # noqa: PT012 - with mock.patch( - "superset.tasks.async_queries.set_form_data" - ) as set_form_data: - set_form_data.side_effect = SoftTimeLimitExceeded() - load_explore_json_into_cache(job_metadata, form_data) - set_form_data.assert_called_once_with(form_data, "error", errors=errors) diff --git a/tests/integration_tests/utils_tests.py b/tests/integration_tests/utils_tests.py index fc083cd7e3ca..a27062f75854 100644 --- a/tests/integration_tests/utils_tests.py +++ b/tests/integration_tests/utils_tests.py @@ -37,7 +37,7 @@ from superset import db, security_manager from superset.constants import NO_TIME_RANGE from superset.exceptions import CertificateException, SupersetException # noqa: F401 -from superset.models.core import Database, Log +from superset.models.core import Database from superset.models.dashboard import Dashboard # noqa: F401 from superset.models.slice import Slice # noqa: F401 from superset.utils.core import ( @@ -63,7 +63,6 @@ from superset.utils.hashing import hash_from_str from superset.views.utils import build_extra_filters, get_form_data # noqa: F401 from tests.integration_tests.base_tests import SupersetTestCase -from tests.integration_tests.constants import ADMIN_USERNAME from tests.integration_tests.fixtures.world_bank_dashboard import ( load_world_bank_dashboard_with_slices, # noqa: F401 load_world_bank_data, # noqa: F401 @@ -354,36 +353,6 @@ def test_get_form_data_corrupted_json(self) -> None: assert form_data == {} assert slc is None - @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") - def test_log_this(self) -> None: - # TODO: Add additional scenarios. - self.login(ADMIN_USERNAME) - slc = self.get_slice("% Rural") - dashboard_id = 1 - - assert slc.viz is not None - resp = self.get_json_resp( # noqa: F841 - f"/explore_json/{slc.datasource_type}/{slc.datasource_id}/" - + f'?form_data={{"slice_id": {slc.id}}}&dashboard_id={dashboard_id}', - {"form_data": json.dumps(slc.viz.form_data)}, - ) - - record = ( - db.session.query(Log) - .filter_by(action="explore_json", slice_id=slc.id) - .order_by(Log.dttm.desc()) - .first() - ) - - assert record.dashboard_id == dashboard_id - assert json.loads(record.json)["dashboard_id"] == str(dashboard_id) - assert json.loads(record.json)["form_data"]["slice_id"] == slc.id - - assert ( - json.loads(record.json)["form_data"]["viz_type"] - == slc.viz.form_data["viz_type"] - ) - def test_schema_validate_json(self): valid = '{"a": 5, "b": [1, 5, ["g", "h"]]}' assert schema.validate_json(valid) is None diff --git a/tests/integration_tests/viz_tests.py b/tests/integration_tests/viz_tests.py deleted file mode 100644 index 272226751fd7..000000000000 --- a/tests/integration_tests/viz_tests.py +++ /dev/null @@ -1,2456 +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. -# isort:skip_file -from datetime import datetime -import logging -from unittest.mock import Mock, patch - -import numpy as np -import pandas as pd -import pytest - -import tests.integration_tests.test_app # noqa: F401 -import superset.viz as viz -from flask import current_app -from superset.exceptions import QueryObjectValidationError, SpatialException -from superset.utils.core import DTTM_ALIAS, ExtraFiltersReasonType -from superset.utils.pandas_postprocessing.utils import FLAT_COLUMN_SEPARATOR -from tests.conftest import with_config - -from .base_tests import SupersetTestCase -from .utils import load_fixture - -logger = logging.getLogger(__name__) - - -class TestBaseViz(SupersetTestCase): - def test_constructor_exception_no_datasource(self): - form_data = {} - datasource = None - with self.assertRaises(Exception): # noqa: B017, PT027 - viz.BaseViz(datasource, form_data) - - def test_process_metrics(self): - # test TimeTableViz metrics in correct order - form_data = { - "url_params": {}, - "row_limit": 500, - "metric": "sum__SP_POP_TOTL", - "entity": "country_code", - "secondary_metric": "sum__SP_POP_TOTL", - "granularity_sqla": "year", - "page_length": 0, - "all_columns": [], - "viz_type": "time_table", - "since": "2014-01-01", - "until": "2014-01-02", - "metrics": ["sum__SP_POP_TOTL", "SUM(SE_PRM_NENR_MA)", "SUM(SP_URB_TOTL)"], - "country_fieldtype": "cca3", - "percent_metrics": ["count"], - "slice_id": 74, - "time_grain_sqla": None, - "order_by_cols": [], - "groupby": ["country_name"], - "compare_lag": "10", - "limit": "25", - "datasource": "2__table", - "table_timestamp_format": "%Y-%m-%d %H:%M:%S", - "markup_type": "markdown", - "where": "", - "compare_suffix": "o10Y", - } - datasource = Mock() - datasource.type = "table" - test_viz = viz.BaseViz(datasource, form_data) - expect_metric_labels = [ - "sum__SP_POP_TOTL", - "SUM(SE_PRM_NENR_MA)", - "SUM(SP_URB_TOTL)", - "count", - ] - assert test_viz.metric_labels == expect_metric_labels - assert test_viz.all_metrics == expect_metric_labels - - def test_get_df_returns_empty_df(self): - form_data = {"dummy": 123} - query_obj = {"granularity": "day"} - datasource = self.get_datasource_mock() - test_viz = viz.BaseViz(datasource, form_data) - result = test_viz.get_df(query_obj) - assert isinstance(result, pd.DataFrame) - assert result.empty - - def test_get_df_handles_dttm_col(self): - form_data = {"dummy": 123} - query_obj = {"granularity": "day"} - results = Mock() - results.query = Mock() - results.status = Mock() - results.error_message = Mock() - datasource = Mock() - datasource.type = "table" - datasource.query = Mock(return_value=results) - mock_dttm_col = Mock() - datasource.get_column = Mock(return_value=mock_dttm_col) - - test_viz = viz.BaseViz(datasource, form_data) - test_viz.df_metrics_to_num = Mock() - test_viz.get_fillna_for_columns = Mock(return_value=0) - - results.df = pd.DataFrame(data={DTTM_ALIAS: ["1960-01-01 05:00:00"]}) - datasource.offset = 0 - mock_dttm_col = Mock() - datasource.get_column = Mock(return_value=mock_dttm_col) - mock_dttm_col.python_date_format = "epoch_ms" - result = test_viz.get_df(query_obj) - import logging # noqa: F401 - - logger.info(result) - pd.testing.assert_series_equal( - result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 5, 0)], name=DTTM_ALIAS) - ) - - mock_dttm_col.python_date_format = None - result = test_viz.get_df(query_obj) - pd.testing.assert_series_equal( - result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 5, 0)], name=DTTM_ALIAS) - ) - - datasource.offset = 1 - result = test_viz.get_df(query_obj) - pd.testing.assert_series_equal( - result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 6, 0)], name=DTTM_ALIAS) - ) - - datasource.offset = 0 - results.df = pd.DataFrame(data={DTTM_ALIAS: ["1960-01-01"]}) - mock_dttm_col.python_date_format = "%Y-%m-%d" - result = test_viz.get_df(query_obj) - pd.testing.assert_series_equal( - result[DTTM_ALIAS], pd.Series([datetime(1960, 1, 1, 0, 0)], name=DTTM_ALIAS) - ) - - def test_cache_timeout(self): - datasource = self.get_datasource_mock() - datasource.cache_timeout = 0 - test_viz = viz.BaseViz(datasource, form_data={}) - assert 0 == test_viz.cache_timeout - - datasource.cache_timeout = 156 - test_viz = viz.BaseViz(datasource, form_data={}) - assert 156 == test_viz.cache_timeout - - datasource.cache_timeout = None - datasource.database.cache_timeout = 0 - assert 0 == test_viz.cache_timeout - - datasource.database.cache_timeout = 1666 - assert 1666 == test_viz.cache_timeout - - datasource.database.cache_timeout = None - test_viz = viz.BaseViz(datasource, form_data={}) - assert ( - current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] - == test_viz.cache_timeout - ) - - data_cache_timeout = current_app.config["DATA_CACHE_CONFIG"][ - "CACHE_DEFAULT_TIMEOUT" - ] - current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] = None - datasource.database.cache_timeout = None - test_viz = viz.BaseViz(datasource, form_data={}) - assert current_app.config["CACHE_DEFAULT_TIMEOUT"] == test_viz.cache_timeout - # restore DATA_CACHE_CONFIG timeout - current_app.config["DATA_CACHE_CONFIG"]["CACHE_DEFAULT_TIMEOUT"] = ( - data_cache_timeout - ) - - -class TestPairedTTest(SupersetTestCase): - def test_get_data_transforms_dataframe(self): - form_data = { - "groupby": ["groupA", "groupB", "groupC"], - "metrics": ["metric1", "metric2", "metric3"], - } - datasource = self.get_datasource_mock() - # Test data - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - pairedTTestViz = viz.viz_types["paired_ttest"](datasource, form_data) # noqa: N806 - data = pairedTTestViz.get_data(df) - # Check method correctly transforms data - expected = { - "metric1": [ - { - "values": [ - {"x": 100, "y": 1}, - {"x": 200, "y": 2}, - {"x": 300, "y": 3}, - ], - "group": ("a1", "a2", "a3"), - }, - { - "values": [ - {"x": 100, "y": 4}, - {"x": 200, "y": 5}, - {"x": 300, "y": 6}, - ], - "group": ("b1", "b2", "b3"), - }, - { - "values": [ - {"x": 100, "y": 7}, - {"x": 200, "y": 8}, - {"x": 300, "y": 9}, - ], - "group": ("c1", "c2", "c3"), - }, - ], - "metric2": [ - { - "values": [ - {"x": 100, "y": 10}, - {"x": 200, "y": 20}, - {"x": 300, "y": 30}, - ], - "group": ("a1", "a2", "a3"), - }, - { - "values": [ - {"x": 100, "y": 40}, - {"x": 200, "y": 50}, - {"x": 300, "y": 60}, - ], - "group": ("b1", "b2", "b3"), - }, - { - "values": [ - {"x": 100, "y": 70}, - {"x": 200, "y": 80}, - {"x": 300, "y": 90}, - ], - "group": ("c1", "c2", "c3"), - }, - ], - "metric3": [ - { - "values": [ - {"x": 100, "y": 100}, - {"x": 200, "y": 200}, - {"x": 300, "y": 300}, - ], - "group": ("a1", "a2", "a3"), - }, - { - "values": [ - {"x": 100, "y": 400}, - {"x": 200, "y": 500}, - {"x": 300, "y": 600}, - ], - "group": ("b1", "b2", "b3"), - }, - { - "values": [ - {"x": 100, "y": 700}, - {"x": 200, "y": 800}, - {"x": 300, "y": 900}, - ], - "group": ("c1", "c2", "c3"), - }, - ], - } - assert data == expected - - def test_get_data_empty_null_keys(self): - form_data = {"groupby": [], "metrics": [""]} - datasource = self.get_datasource_mock() - # Test data - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300] - raw[""] = [1, 2, 3] - raw[None] = [10, 20, 30] - - df = pd.DataFrame(raw) - pairedTTestViz = viz.viz_types["paired_ttest"](datasource, form_data) # noqa: N806 - data = pairedTTestViz.get_data(df) - # Check method correctly transforms data - expected = { - "N/A": [ - { - "values": [ - {"x": 100, "y": 1}, - {"x": 200, "y": 2}, - {"x": 300, "y": 3}, - ], - "group": "All", - } - ], - } - assert data == expected - - form_data = {"groupby": [], "metrics": [None]} - with self.assertRaises(ValueError): # noqa: PT027 - viz.viz_types["paired_ttest"](datasource, form_data) - - -class TestPartitionViz(SupersetTestCase): - @patch("superset.viz.BaseViz.query_obj") - def test_query_obj_time_series_option(self, super_query_obj): - datasource = self.get_datasource_mock() - form_data = {} - test_viz = viz.PartitionViz(datasource, form_data) - super_query_obj.return_value = {} - query_obj = test_viz.query_obj() - assert not query_obj["is_timeseries"] - test_viz.form_data["time_series_option"] = "agg_sum" - query_obj = test_viz.query_obj() - assert query_obj["is_timeseries"] - - def test_levels_for_computes_levels(self): - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - groups = ["groupA", "groupB", "groupC"] - time_op = "agg_sum" - test_viz = viz.PartitionViz(Mock(), {}) - levels = test_viz.levels_for(time_op, groups, df) - assert 4 == len(levels) - expected = {DTTM_ALIAS: 1800, "metric1": 45, "metric2": 450, "metric3": 4500} - assert expected == levels[0].to_dict() - expected = { - DTTM_ALIAS: {"a1": 600, "b1": 600, "c1": 600}, - "metric1": {"a1": 6, "b1": 15, "c1": 24}, - "metric2": {"a1": 60, "b1": 150, "c1": 240}, - "metric3": {"a1": 600, "b1": 1500, "c1": 2400}, - } - assert expected == levels[1].to_dict() - assert ["groupA", "groupB"] == levels[2].index.names - assert ["groupA", "groupB", "groupC"] == levels[3].index.names - time_op = "agg_mean" - levels = test_viz.levels_for(time_op, groups, df) - assert 4 == len(levels) - expected = { - DTTM_ALIAS: 200.0, - "metric1": 5.0, - "metric2": 50.0, - "metric3": 500.0, - } - assert expected == levels[0].to_dict() - expected = { - DTTM_ALIAS: {"a1": 200, "c1": 200, "b1": 200}, - "metric1": {"a1": 2, "b1": 5, "c1": 8}, - "metric2": {"a1": 20, "b1": 50, "c1": 80}, - "metric3": {"a1": 200, "b1": 500, "c1": 800}, - } - assert expected == levels[1].to_dict() - assert ["groupA", "groupB"] == levels[2].index.names - assert ["groupA", "groupB", "groupC"] == levels[3].index.names - - def test_levels_for_diff_computes_difference(self): - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - groups = ["groupA", "groupB", "groupC"] - test_viz = viz.PartitionViz(Mock(), {}) - time_op = "point_diff" - levels = test_viz.levels_for_diff(time_op, groups, df) - expected = {"metric1": 6, "metric2": 60, "metric3": 600} - assert expected == levels[0].to_dict() - expected = { - "metric1": {"a1": 2, "b1": 2, "c1": 2}, - "metric2": {"a1": 20, "b1": 20, "c1": 20}, - "metric3": {"a1": 200, "b1": 200, "c1": 200}, - } - assert expected == levels[1].to_dict() - assert 4 == len(levels) - assert ["groupA", "groupB", "groupC"] == levels[3].index.names - - def test_levels_for_time_calls_process_data_and_drops_cols(self): - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - groups = ["groupA", "groupB", "groupC"] - test_viz = viz.PartitionViz(Mock(), {"groupby": groups}) - - def return_args(df_drop, aggregate): - return df_drop - - test_viz.process_data = Mock(side_effect=return_args) - levels = test_viz.levels_for_time(groups, df) - assert 4 == len(levels) - cols = [DTTM_ALIAS, "metric1", "metric2", "metric3"] - assert sorted(cols) == sorted(levels[0].columns.tolist()) - cols += ["groupA"] - assert sorted(cols) == sorted(levels[1].columns.tolist()) - cols += ["groupB"] - assert sorted(cols) == sorted(levels[2].columns.tolist()) - cols += ["groupC"] - assert sorted(cols) == sorted(levels[3].columns.tolist()) - assert 4 == len(test_viz.process_data.mock_calls) - - def test_nest_values_returns_hierarchy(self): - raw = {} - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - test_viz = viz.PartitionViz(Mock(), {}) - groups = ["groupA", "groupB", "groupC"] - levels = test_viz.levels_for("agg_sum", groups, df) - nest = test_viz.nest_values(levels) - assert 3 == len(nest) - for i in range(0, 3): - assert "metric" + str(i + 1) == nest[i]["name"] - assert 3 == len(nest[0]["children"]) - assert 1 == len(nest[0]["children"][0]["children"]) - assert 1 == len(nest[0]["children"][0]["children"][0]["children"]) - - def test_nest_values_returns_hierarchy_when_more_dimensions(self): - raw = {} - raw["category"] = ["a", "a", "a"] - raw["subcategory"] = ["a.2", "a.1", "a.2"] - raw["sub_subcategory"] = ["a.2.1", "a.1.1", "a.2.2"] - raw["metric1"] = [5, 10, 15] - raw["metric2"] = [50, 100, 150] - raw["metric3"] = [500, 1000, 1500] - df = pd.DataFrame(raw) - test_viz = viz.PartitionViz(Mock(), {}) - groups = ["category", "subcategory", "sub_subcategory"] - levels = test_viz.levels_for("agg_sum", groups, df) - nest = test_viz.nest_values(levels) - assert 3 == len(nest) - for i in range(0, 3): - assert "metric" + str(i + 1) == nest[i]["name"] - assert 1 == len(nest[0]["children"]) - assert 2 == len(nest[0]["children"][0]["children"]) - assert 1 == len(nest[0]["children"][0]["children"][0]["children"]) - assert 2 == len(nest[0]["children"][0]["children"][1]["children"]) - - def test_nest_procs_returns_hierarchy(self): - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - test_viz = viz.PartitionViz(Mock(), {}) - groups = ["groupA", "groupB", "groupC"] - metrics = ["metric1", "metric2", "metric3"] - procs = {} - for i in range(0, 4): - df_drop = df.drop(groups[i:], axis=1) - pivot = df_drop.pivot_table( - index=DTTM_ALIAS, columns=groups[:i], values=metrics - ) - procs[i] = pivot - nest = test_viz.nest_procs(procs) - assert 3 == len(nest) - for i in range(0, 3): - assert "metric" + str(i + 1) == nest[i]["name"] - assert None is nest[i].get("val") - assert 3 == len(nest[0]["children"]) - assert 3 == len(nest[0]["children"][0]["children"]) - assert 1 == len(nest[0]["children"][0]["children"][0]["children"]) - assert 1 == len( - nest[0]["children"][0]["children"][0]["children"][0]["children"] - ) - - def test_get_data_calls_correct_method(self): - raw = {} - raw[DTTM_ALIAS] = [100, 200, 300, 100, 200, 300, 100, 200, 300] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - raw["metric2"] = [10, 20, 30, 40, 50, 60, 70, 80, 90] - raw["metric3"] = [100, 200, 300, 400, 500, 600, 700, 800, 900] - df = pd.DataFrame(raw) - test_viz = viz.PartitionViz(Mock(), {}) - with self.assertRaises(ValueError): # noqa: PT027 - test_viz.get_data(df) - test_viz.levels_for = Mock(return_value=1) - test_viz.nest_values = Mock(return_value=1) - test_viz.form_data["groupby"] = ["groups"] - test_viz.form_data["time_series_option"] = "not_time" - test_viz.get_data(df) - assert "agg_sum" == test_viz.levels_for.mock_calls[0][1][0] - test_viz.form_data["time_series_option"] = "agg_sum" - test_viz.get_data(df) - assert "agg_sum" == test_viz.levels_for.mock_calls[1][1][0] - test_viz.form_data["time_series_option"] = "agg_mean" - test_viz.get_data(df) - assert "agg_mean" == test_viz.levels_for.mock_calls[2][1][0] - test_viz.form_data["time_series_option"] = "point_diff" - test_viz.levels_for_diff = Mock(return_value=1) - test_viz.get_data(df) - assert "point_diff" == test_viz.levels_for_diff.mock_calls[0][1][0] - test_viz.form_data["time_series_option"] = "point_percent" - test_viz.get_data(df) - assert "point_percent" == test_viz.levels_for_diff.mock_calls[1][1][0] - test_viz.form_data["time_series_option"] = "point_factor" - test_viz.get_data(df) - assert "point_factor" == test_viz.levels_for_diff.mock_calls[2][1][0] - test_viz.levels_for_time = Mock(return_value=1) - test_viz.nest_procs = Mock(return_value=1) - test_viz.form_data["time_series_option"] = "adv_anal" - test_viz.get_data(df) - assert 1 == len(test_viz.levels_for_time.mock_calls) - assert 1 == len(test_viz.nest_procs.mock_calls) - test_viz.form_data["time_series_option"] = "time_series" - test_viz.get_data(df) - assert "agg_sum" == test_viz.levels_for.mock_calls[3][1][0] - assert 7 == len(test_viz.nest_values.mock_calls) - - -class TestRoseVis(SupersetTestCase): - def test_rose_vis_get_data(self): - raw = {} - t1 = pd.Timestamp("2000") - t2 = pd.Timestamp("2002") - t3 = pd.Timestamp("2004") - raw[DTTM_ALIAS] = [t1, t2, t3, t1, t2, t3, t1, t2, t3] - raw["groupA"] = ["a1", "a1", "a1", "b1", "b1", "b1", "c1", "c1", "c1"] - raw["groupB"] = ["a2", "a2", "a2", "b2", "b2", "b2", "c2", "c2", "c2"] - raw["groupC"] = ["a3", "a3", "a3", "b3", "b3", "b3", "c3", "c3", "c3"] - raw["metric1"] = [1, 2, 3, 4, 5, 6, 7, 8, 9] - df = pd.DataFrame(raw) - fd = {"metrics": ["metric1"], "groupby": ["groupA"]} - test_viz = viz.RoseViz(Mock(), fd) - test_viz.metrics = fd["metrics"] - res = test_viz.get_data(df) - expected = { - 946684800000000000: [ - {"time": t1, "value": 1, "key": ("a1",), "name": ("a1",)}, - {"time": t1, "value": 4, "key": ("b1",), "name": ("b1",)}, - {"time": t1, "value": 7, "key": ("c1",), "name": ("c1",)}, - ], - 1009843200000000000: [ - {"time": t2, "value": 2, "key": ("a1",), "name": ("a1",)}, - {"time": t2, "value": 5, "key": ("b1",), "name": ("b1",)}, - {"time": t2, "value": 8, "key": ("c1",), "name": ("c1",)}, - ], - 1072915200000000000: [ - {"time": t3, "value": 3, "key": ("a1",), "name": ("a1",)}, - {"time": t3, "value": 6, "key": ("b1",), "name": ("b1",)}, - {"time": t3, "value": 9, "key": ("c1",), "name": ("c1",)}, - ], - } - assert expected == res - - -class TestTimeSeriesTableViz(SupersetTestCase): - def test_get_data_metrics(self): - form_data = {"metrics": ["sum__A", "count"], "groupby": []} - datasource = self.get_datasource_mock() - raw = {} - t1 = pd.Timestamp("2000") - t2 = pd.Timestamp("2002") - raw[DTTM_ALIAS] = [t1, t2] - raw["sum__A"] = [15, 20] - raw["count"] = [6, 7] - df = pd.DataFrame(raw) - test_viz = viz.TimeTableViz(datasource, form_data) - data = test_viz.get_data(df) - # Check method correctly transforms data - assert {"count", "sum__A"} == set(data["columns"]) - time_format = "%Y-%m-%d %H:%M:%S" - expected = { - t1.strftime(time_format): {"sum__A": 15, "count": 6}, - t2.strftime(time_format): {"sum__A": 20, "count": 7}, - } - assert expected == data["records"] - - def test_get_data_group_by(self): - form_data = {"metrics": ["sum__A"], "groupby": ["groupby1"]} - datasource = self.get_datasource_mock() - raw = {} - t1 = pd.Timestamp("2000") - t2 = pd.Timestamp("2002") - raw[DTTM_ALIAS] = [t1, t1, t1, t2, t2, t2] - raw["sum__A"] = [15, 20, 25, 30, 35, 40] - raw["groupby1"] = ["a1", "a2", "a3", "a1", "a2", "a3"] - df = pd.DataFrame(raw) - test_viz = viz.TimeTableViz(datasource, form_data) - data = test_viz.get_data(df) - # Check method correctly transforms data - assert {"a1", "a2", "a3"} == set(data["columns"]) - time_format = "%Y-%m-%d %H:%M:%S" - expected = { - t1.strftime(time_format): {"a1": 15, "a2": 20, "a3": 25}, - t2.strftime(time_format): {"a1": 30, "a2": 35, "a3": 40}, - } - assert expected == data["records"] - - def test_get_data_multiple_group_by(self): - sep = FLAT_COLUMN_SEPARATOR - form_data = {"metrics": ["sum__A"], "groupby": ["groupby1", "groupby2"]} - datasource = self.get_datasource_mock() - raw = {} - t1 = pd.Timestamp("2000") - t2 = pd.Timestamp("2002") - raw[DTTM_ALIAS] = [t1, t1, t1, t1, t2, t2, t2, t2] - raw["sum__A"] = [15, 20, 25, 30, 35, 40, 45, 50] - raw["groupby1"] = ["a1", "a2", "a1", "a2", "a1", "a2", "a1", "a2"] - raw["groupby2"] = ["b1", "b1", "b2", "b2", "b1", "b1", "b2", "b2"] - df = pd.DataFrame(raw) - test_viz = viz.TimeTableViz(datasource, form_data) - data = test_viz.get_data(df) - # Columns should be flattened strings, not tuples - a1_b1 = f"a1{sep}b1" - a1_b2 = f"a1{sep}b2" - a2_b1 = f"a2{sep}b1" - a2_b2 = f"a2{sep}b2" - assert {a1_b1, a1_b2, a2_b1, a2_b2} == set(data["columns"]) - time_format = "%Y-%m-%d %H:%M:%S" - expected = { - t1.strftime(time_format): { - a1_b1: 15, - a1_b2: 25, - a2_b1: 20, - a2_b2: 30, - }, - t2.strftime(time_format): { - a1_b1: 35, - a1_b2: 45, - a2_b1: 40, - a2_b2: 50, - }, - } - assert expected == data["records"] - assert data["is_group_by"] is True - - @patch("superset.viz.BaseViz.query_obj") - def test_query_obj_throws_metrics_and_groupby(self, super_query_obj): - datasource = self.get_datasource_mock() - form_data = {"groupby": ["a"]} - super_query_obj.return_value = {} - test_viz = viz.TimeTableViz(datasource, form_data) - with self.assertRaises(Exception): # noqa: B017, PT027 - test_viz.query_obj() - form_data["metrics"] = ["x", "y"] - test_viz = viz.TimeTableViz(datasource, form_data) - with self.assertRaises(Exception): # noqa: B017, PT027 - test_viz.query_obj() - - def test_query_obj_order_by(self): - test_viz = viz.TimeTableViz( - self.get_datasource_mock(), {"metrics": ["sum__A", "count"], "groupby": []} - ) - query_obj = test_viz.query_obj() - assert query_obj["orderby"] == [("sum__A", False)] - - -class TestBaseDeckGLViz(SupersetTestCase): - def test_get_metrics(self): - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - result = test_viz_deckgl.get_metrics() - assert result == [form_data.get("size")] - - form_data = {} - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - result = test_viz_deckgl.get_metrics() - assert result == [] - - def test_extract_tooltip_columns_string_items(self): - """Test _extract_tooltip_columns with string items in tooltip_contents""" - form_data = {"tooltip_contents": ["column1", "column2", "column3"]} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._extract_tooltip_columns() - assert result == ["column1", "column2", "column3"] - - def test_extract_tooltip_columns_dict_items(self): - """Test _extract_tooltip_columns with dict items in tooltip_contents""" - form_data = { - "tooltip_contents": [ - {"item_type": "column", "column_name": "LAT"}, - {"item_type": "column", "column_name": "LON"}, - {"item_type": "metric", "metric_name": "count"}, - {"item_type": "column", "column_name": "CITY"}, - ] - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._extract_tooltip_columns() - assert result == ["LAT", "LON", "CITY"] - - def test_extract_tooltip_columns_mixed_items(self): - """Test _extract_tooltip_columns with mixed string and dict items""" - form_data = { - "tooltip_contents": [ - "string_column", - {"item_type": "column", "column_name": "dict_column"}, - { - "item_type": "invalid", - "column_name": "invalid_column", - }, - {"item_type": "column"}, - ] - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._extract_tooltip_columns() - assert result == ["string_column", "dict_column"] - - def test_extract_tooltip_columns_empty(self): - """Test _extract_tooltip_columns with empty or missing tooltip_contents""" - form_data = {"tooltip_contents": []} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._extract_tooltip_columns() - assert result == [] - - form_data = {} - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._extract_tooltip_columns() - assert result == [] - - def test_add_tooltip_columns_to_query_with_metrics(self): - """Test _add_tooltip_columns_to_query when query has metrics""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - query_obj = { - "metrics": ["count", "avg_value"], - "groupby": ["existing_column"], - "columns": [], - } - tooltip_columns = ["tooltip_col1", "tooltip_col2", "existing_column"] - - test_viz._add_tooltip_columns_to_query(query_obj, tooltip_columns) - - # Should add to groupby, avoiding duplicates - assert "tooltip_col1" in query_obj["groupby"] - assert "tooltip_col2" in query_obj["groupby"] - assert query_obj["groupby"].count("existing_column") == 1 # No duplicate - - def test_add_tooltip_columns_to_query_without_metrics(self): - """Test _add_tooltip_columns_to_query when query has no metrics""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - query_obj = {"metrics": [], "groupby": [], "columns": ["existing_column"]} - tooltip_columns = ["tooltip_col1", "tooltip_col2", "existing_column"] - - test_viz._add_tooltip_columns_to_query(query_obj, tooltip_columns) - - # Should add to columns, avoiding duplicates - assert "tooltip_col1" in query_obj["columns"] - assert "tooltip_col2" in query_obj["columns"] - assert query_obj["columns"].count("existing_column") == 1 # No duplicate - - def test_add_tooltip_columns_to_query_empty_columns(self): - """Test _add_tooltip_columns_to_query with empty tooltip columns""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - query_obj = {"metrics": [], "groupby": [], "columns": []} - original_query_obj = query_obj.copy() - - test_viz._add_tooltip_columns_to_query(query_obj, []) - - # Should not modify query_obj - assert query_obj == original_query_obj - - def test_integrate_tooltip_columns(self): - """Test _integrate_tooltip_columns helper method""" - form_data = {"tooltip_contents": ["LON", "LAT"]} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - query_obj = {"metrics": ["count"], "groupby": ["region"]} - - result = test_viz._integrate_tooltip_columns(query_obj) - - assert "LON" in result["groupby"] - assert "LAT" in result["groupby"] - assert "region" in result["groupby"] - assert result is query_obj # Should return the same object - - def test_add_tooltip_properties(self): - """Test _add_tooltip_properties helper method""" - form_data = {"tooltip_contents": ["CITY", "POPULATION", "missing_column"]} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - properties = {"position": [1, 2], "weight": 100} - data = {"CITY": "New York", "POPULATION": 8000000, "other_column": "value"} - - result = test_viz._add_tooltip_properties(properties, data) - - assert result["CITY"] == "New York" - assert result["POPULATION"] == 8000000 - assert "missing_column" not in result # Missing column should not be added - assert "other_column" not in result # Non-tooltip column should not be added - assert result["position"] == [1, 2] # Original properties should remain - assert result["weight"] == 100 - assert result is properties # Should return the same object - - def test_get_base_properties(self): - """Test _get_base_properties helper method""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric_label = "test_metric" - - data = {"spatial": [10.5, 20.3], "test_metric": 42, "other_field": "value"} - - result = test_viz._get_base_properties(data) - - assert result["position"] == [10.5, 20.3] - assert result["weight"] == 42 - assert len(result) == 2 # Should only have position and weight - - def test_get_base_properties_no_metric(self): - """Test _get_base_properties when no metric is available""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric_label = None - - data = {"spatial": [10.5, 20.3]} - - result = test_viz._get_base_properties(data) - - assert result["position"] == [10.5, 20.3] - assert result["weight"] == 1 # Should default to 1 - - def test_setup_metric_label(self): - """Test _setup_metric_label helper method""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric = "SUM(sales)" - - with patch("superset.utils.core.get_metric_name") as mock_get_metric_name: - mock_get_metric_name.return_value = "sum__sales" - - test_viz._setup_metric_label() - - assert test_viz.metric_label == "sum__sales" - mock_get_metric_name.assert_called_once_with("SUM(sales)") - - def test_setup_metric_label_no_metric(self): - """Test _setup_metric_label when metric is None""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric = None - - test_viz._setup_metric_label() - - assert test_viz.metric_label is None - - def test_scatterviz_get_metrics(self): - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - - form_data = {} - test_viz_deckgl = viz.DeckScatterViz(datasource, form_data) - test_viz_deckgl.point_radius_fixed = {"type": "metric", "value": "int"} - result = test_viz_deckgl.get_metrics() - assert result == ["int"] - - form_data = {} - test_viz_deckgl = viz.DeckScatterViz(datasource, form_data) - test_viz_deckgl.point_radius_fixed = {} - result = test_viz_deckgl.get_metrics() - assert result == [] - - def test_get_js_columns(self): - form_data = { - **load_fixture("deck_path_form_data.json"), - "js_columns": ["color"], - } - datasource = self.get_datasource_mock() - mock_d = {"a": "dummy1", "b": "dummy2", "c": "dummy3"} - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - result = test_viz_deckgl.get_js_columns(mock_d) - - assert result == {"color": None} - - def test_get_properties(self): - mock_d = {} - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - - with self.assertRaises(NotImplementedError) as context: # noqa: PT027 - test_viz_deckgl.get_properties(mock_d) - - assert "" in str(context.exception) - - def test_process_spatial_query_obj(self): - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - mock_key = "spatial_key" - mock_gb = [] - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - - with self.assertRaises(ValueError) as context: # noqa: PT027 - test_viz_deckgl.process_spatial_query_obj(mock_key, mock_gb) - - assert "Bad spatial key" in str(context.exception) - - test_form_data = { - "latlong_key": {"type": "latlong", "lonCol": "lon", "latCol": "lat"}, - "delimited_key": {"type": "delimited", "lonlatCol": "lonlat"}, - "geohash_key": {"type": "geohash", "geohashCol": "geo"}, - } - - datasource = self.get_datasource_mock() - expected_results = { - "latlong_key": ["lon", "lat"], - "delimited_key": ["lonlat"], - "geohash_key": ["geo"], - } - for mock_key in ["latlong_key", "delimited_key", "geohash_key"]: - mock_gb = [] - test_viz_deckgl = viz.BaseDeckGLViz(datasource, test_form_data) - test_viz_deckgl.process_spatial_query_obj(mock_key, mock_gb) - assert expected_results.get(mock_key) == mock_gb - - def test_geojson_query_obj(self): - form_data = load_fixture("deck_geojson_form_data.json") - datasource = self.get_datasource_mock() - test_viz_deckgl = viz.DeckGeoJson(datasource, form_data) - results = test_viz_deckgl.query_obj() - - assert results["metrics"] == [] - assert results["groupby"] == [] - assert results["columns"] == ["test_col"] - - def test_parse_coordinates(self): - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - viz_instance = viz.BaseDeckGLViz(datasource, form_data) - - coord = viz_instance.parse_coordinates("1.23, 3.21") - assert coord == (1.23, 3.21) - - coord = viz_instance.parse_coordinates("1.23 3.21") - assert coord == (1.23, 3.21) - - assert viz_instance.parse_coordinates(None) is None - - assert viz_instance.parse_coordinates("") is None - - def test_parse_coordinates_raises(self): - form_data = load_fixture("deck_path_form_data.json") - datasource = self.get_datasource_mock() - test_viz_deckgl = viz.BaseDeckGLViz(datasource, form_data) - - with self.assertRaises(SpatialException): # noqa: PT027 - test_viz_deckgl.parse_coordinates("NULL") - - with self.assertRaises(SpatialException): # noqa: PT027 - test_viz_deckgl.parse_coordinates("fldkjsalkj,fdlaskjfjadlksj") - - def test_deckscatter_query_obj_integration(self): - """Test DeckScatterViz query_obj uses new tooltip integration""" - form_data = { - "tooltip_contents": ["CITY", "POPULATION"], - "time_grain_sqla": "P1D", - "point_radius_fixed": {"type": "fix", "value": 500}, - "spatial": { - "type": "latlong", - "lonCol": "longitude", - "latCol": "latitude", - }, - } - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - - with patch.object(test_viz, "_integrate_tooltip_columns") as mock_integrate: - mock_integrate.return_value = {"mocked": "result"} - - result = test_viz.query_obj() - - # Should call the new helper method - mock_integrate.assert_called_once() - assert result == {"mocked": "result"} - - # Should set up instance variables - assert test_viz.is_timeseries is True - assert test_viz.point_radius_fixed == {"type": "fix", "value": 500} - - def test_deckscatter_query_obj_no_time_grain(self): - """Test DeckScatterViz query_obj when time_grain_sqla is not set""" - form_data = { - "tooltip_contents": ["CITY"], - "point_radius_fixed": {"type": "metric", "value": "count"}, - "spatial": { - "type": "latlong", - "lonCol": "longitude", - "latCol": "latitude", - }, - } - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - - with patch.object(test_viz, "_integrate_tooltip_columns") as mock_integrate: - mock_integrate.return_value = {"result": "data"} - - test_viz.query_obj() - - # Should set timeseries to False when no time_grain_sqla - assert test_viz.is_timeseries is False - - def test_deckscatter_get_properties_uses_helper(self): - """Test DeckScatterViz get_properties uses new helper method""" - form_data = {"tooltip_contents": ["CITY", "STATE"], "dimension": "region"} - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - test_viz.metric_label = "count" - test_viz.fixed_value = 10 - test_viz.dim = "region" - - data = { - "spatial": [40.7, -74.0], - "count": 100, - "region": "northeast", - "CITY": "New York", - "STATE": "NY", - "__timestamp": "2023-01-01", - } - - with patch.object(test_viz, "_add_tooltip_properties") as mock_add_tooltip: - mock_add_tooltip.return_value = {"final": "result"} - - result = test_viz.get_properties(data) - - expected_base_properties = { - "metric": 100, - "radius": 10, - "cat_color": "northeast", - "position": [40.7, -74.0], - "__timestamp": "2023-01-01", - } - mock_add_tooltip.assert_called_once_with(expected_base_properties, data) - assert result == {"final": "result"} - - def test_deckscatter_get_data_uses_setup_metric_label(self): - """Test DeckScatterViz get_data uses new _setup_metric_label helper""" - form_data = { - "point_radius_fixed": {"type": "metric", "value": "avg_sales"}, - "dimension": "category", - } - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - test_df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]}) - - with patch.object(test_viz, "_setup_metric_label") as mock_setup: - with patch("superset.viz.BaseDeckGLViz.get_data") as mock_super_get_data: - mock_super_get_data.return_value = {"test": "data"} - - result = test_viz.get_data(test_df) - - mock_setup.assert_called_once() - - assert test_viz.point_radius_fixed == { - "type": "metric", - "value": "avg_sales", - } - assert test_viz.fixed_value is None - assert test_viz.dim == "category" - - mock_super_get_data.assert_called_once_with(test_df) - assert result == {"test": "data"} - - def test_deckscatter_get_data_with_fixed_radius(self): - """Test DeckScatterViz get_data with fixed radius type""" - form_data = { - "point_radius_fixed": {"type": "fix", "value": 25}, - "dimension": "location", - } - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - test_df = pd.DataFrame({"col1": [1, 2]}) - - with patch.object(test_viz, "_setup_metric_label"): - with patch("superset.viz.BaseDeckGLViz.get_data") as mock_super_get_data: - mock_super_get_data.return_value = {"result": "ok"} - - test_viz.get_data(test_df) - - # Should set fixed_value when type is not 'metric' - assert test_viz.fixed_value == 25 - - def test_tooltip_integration_end_to_end(self): - """Test complete tooltip integration from query_obj to get_properties""" - form_data = { - "tooltip_contents": ["CITY", "POPULATION"], - "size": "count", - "time_grain_sqla": None, - } - datasource = self.get_datasource_mock() - test_viz = viz.DeckScatterViz(datasource, form_data) - - with patch("superset.viz.BaseDeckGLViz.query_obj") as mock_super_query: - mock_super_query.return_value = { - "metrics": ["count"], - "groupby": ["region"], - "columns": [], - } - - query_result = test_viz.query_obj() - - # Should have added tooltip columns to groupby - assert "CITY" in query_result["groupby"] - assert "POPULATION" in query_result["groupby"] - assert "region" in query_result["groupby"] - - test_viz.metric_label = "count" - test_viz.fixed_value = None - test_viz.dim = "region" - - data = { - "spatial": [40.7, -74.0], - "count": 5000, - "region": "northeast", - "CITY": "New York", - "POPULATION": 8000000, - "__timestamp": "2023-01-01", - } - - properties_result = test_viz.get_properties(data) - - # Should include both base properties and tooltip properties - assert properties_result["position"] == [40.7, -74.0] - assert properties_result["metric"] == 5000 - assert properties_result["cat_color"] == "northeast" - assert properties_result["__timestamp"] == "2023-01-01" - assert properties_result["CITY"] == "New York" - assert properties_result["POPULATION"] == 8000000 - - def test_filter_nulls(self): - test_form_data = { - "latlong_key": {"type": "latlong", "lonCol": "lon", "latCol": "lat"}, - "delimited_key": {"type": "delimited", "lonlatCol": "lonlat"}, - "geohash_key": {"type": "geohash", "geohashCol": "geo"}, - } - - datasource = self.get_datasource_mock() - # SHA-256 filterOptionName hashes with default HASH_ALGORITHM - expected_results = { - "latlong_key": [ - { - "clause": "WHERE", - "expressionType": "SIMPLE", - "filterOptionName": ( - "980dd3068274177120307d9182ea8e8ee1b7824d34fbc21c529441f5d3279f7f" - ), - "comparator": "", - "operator": "IS NOT NULL", - "subject": "lat", - }, - { - "clause": "WHERE", - "expressionType": "SIMPLE", - "filterOptionName": ( - "e368c259da27e5ec6a854772d9bff2c2af8dd5762352cef4ff6afc5bd8b6b9ea" - ), - "comparator": "", - "operator": "IS NOT NULL", - "subject": "lon", - }, - ], - "delimited_key": [ - { - "clause": "WHERE", - "expressionType": "SIMPLE", - "filterOptionName": ( - "6ea33b70ab781033af421240019d3e3ad782928a3ad2999538f1f4b2a52305e2" - ), - "comparator": "", - "operator": "IS NOT NULL", - "subject": "lonlat", - } - ], - "geohash_key": [ - { - "clause": "WHERE", - "expressionType": "SIMPLE", - "filterOptionName": ( - "48bbd94cd6afb1885d8550e2928bc01a2d3bc7d1f4f1d0929b10d6f4021b7f14" - ), - "comparator": "", - "operator": "IS NOT NULL", - "subject": "geo", - } - ], - } - for mock_key in ["latlong_key", "delimited_key", "geohash_key"]: - test_viz_deckgl = viz.BaseDeckGLViz(datasource, test_form_data.copy()) - test_viz_deckgl.spatial_control_keys = [mock_key] - test_viz_deckgl.add_null_filters() - adhoc_filters = test_viz_deckgl.form_data["adhoc_filters"] - assert expected_results.get(mock_key) == adhoc_filters - - def test_init_with_layer_filtering_applied(self): - """Test BaseDeckGLViz.__init__ applies layer filtering when conditions are - met.""" - datasource = self.get_datasource_mock() - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - "deck_slices": [123, 456], - }, - { - "clause": "WHERE", - "subject": "col2", - "operator": "!=", - "comparator": "value2", - "layerFilterScope": [1], - "deck_slices": [123, 456], - }, - ], - } - - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - # Should only have the first filter (scoped to layer 0) - assert len(test_viz.form_data["adhoc_filters"]) == 1 - assert test_viz.form_data["adhoc_filters"][0]["subject"] == "col1" - - def test_init_without_layer_filtering(self): - """Test BaseDeckGLViz.__init__ doesn't apply filtering when conditions - aren't met.""" - datasource = self.get_datasource_mock() - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ] - } - original_filters = form_data["adhoc_filters"].copy() - - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - # Filters should remain unchanged - assert test_viz.form_data["adhoc_filters"] == original_filters - - def test_should_apply_layer_filtering_true(self): - """Test _should_apply_layer_filtering returns True when all conditions are - met.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = {"slice_id": 123, "adhoc_filters": [{"layerFilterScope": [0, 1]}]} - - result = test_viz._should_apply_layer_filtering(form_data) - assert result is True - - def test_should_apply_layer_filtering_false_missing_slice_id(self): - """Test _should_apply_layer_filtering returns False when slice_id is missing.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = {"adhoc_filters": [{"layerFilterScope": [0, 1]}]} - - result = test_viz._should_apply_layer_filtering(form_data) - assert result is False - - def test_should_apply_layer_filtering_false_missing_adhoc_filters(self): - """Test _should_apply_layer_filtering returns False when - adhoc_filters missing""" - form_data = {"slice_id": 123} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is False - - def test_should_apply_layer_filtering_false_no_layer_scoped_filters(self): - """Test _should_apply_layer_filtering returns False when no - layer scoped filters""" - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is False - - def test_has_layer_scoped_filters_true_with_dict(self): - """Test _has_layer_scoped_filters returns True when filter - has layerFilterScope""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is True - - def test_has_layer_scoped_filters_true_with_non_none_value(self): - """Test _has_layer_scoped_filters returns True when layerFilterScope not None""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is True - - def test_has_layer_scoped_filters_false_none_value(self): - """Test _has_layer_scoped_filters returns False when layerFilterScope is None""" - form_data = {"adhoc_filters": [{"layerFilterScope": None}, {"clause": "WHERE"}]} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is False - - def test_has_layer_scoped_filters_false_no_scoped_filters(self): - """Test _has_layer_scoped_filters returns False when no filters - have layerFilterScope""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is False - - def test_apply_multilayer_filtering_filters_by_layer_scope(self): - """Test _apply_multilayer_filtering correctly filters by layer scope.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = { - "slice_id": 456, # This is layer index 1 - "adhoc_filters": [ - { - "subject": "global_filter", - "deck_slices": [123, 456], - # No layerFilterScope = global filter - }, - { - "subject": "layer_0_filter", - "layerFilterScope": [0], - "deck_slices": [123, 456], - }, - { - "subject": "layer_1_filter", - "layerFilterScope": [1], - "deck_slices": [123, 456], - }, - { - "subject": "layer_0_1_filter", - "layerFilterScope": [0, 1], - "deck_slices": [123, 456], - }, - ], - } - - result = test_viz._apply_multilayer_filtering(form_data) - - # Should include: global_filter, layer_1_filter, layer_0_1_filter - assert len(result["adhoc_filters"]) == 3 - subjects = [f["subject"] for f in result["adhoc_filters"]] - assert "global_filter" in subjects - assert "layer_1_filter" in subjects - assert "layer_0_1_filter" in subjects - assert "layer_0_filter" not in subjects - - def test_apply_multilayer_filtering_no_deck_slices(self): - """Test _apply_multilayer_filtering returns original form_data when no - deck_slices.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = {"slice_id": 123, "adhoc_filters": [{"subject": "filter1"}]} - - result = test_viz._apply_multilayer_filtering(form_data) - - # Should return original form_data unchanged - assert result == form_data - - def test_apply_multilayer_filtering_slice_not_in_deck_slices(self): - """Test _apply_multilayer_filtering returns original when slice_id not in - deck_slices.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = { - "slice_id": 999, # Not in deck_slices - "adhoc_filters": [{"subject": "filter1", "deck_slices": [123, 456]}], - } - - result = test_viz._apply_multilayer_filtering(form_data) - - # Should return original form_data unchanged - assert result == form_data - - def test_get_deck_slices_from_filters_found(self): - """Test _get_deck_slices_from_filters returns deck_slices when found.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = { - "adhoc_filters": [ - {"subject": "filter1"}, - {"subject": "filter2", "deck_slices": [123, 456, 789]}, - {"subject": "filter3"}, - ] - } - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result == [123, 456, 789] - - def test_get_deck_slices_from_filters_not_found(self): - """Test _get_deck_slices_from_filters returns None when not found.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = {"adhoc_filters": [{"subject": "filter1"}, {"subject": "filter2"}]} - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result is None - - def test_get_deck_slices_from_filters_empty_filters(self): - """Test _get_deck_slices_from_filters returns None when adhoc_filters is - empty.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - form_data = {"adhoc_filters": []} - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result is None - - def test_get_filter_layer_scope_dict(self): - """Test _get_filter_layer_scope with dict filter item.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - filter_item = {"layerFilterScope": [0, 1, 2]} - result = test_viz._get_filter_layer_scope(filter_item) - - assert result == [0, 1, 2] - - def test_get_filter_layer_scope_dict_none(self): - """Test _get_filter_layer_scope with dict that has None layerFilterScope.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - filter_item = {"layerFilterScope": None} - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_dict_missing_key(self): - """Test _get_filter_layer_scope with dict missing layerFilterScope.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - filter_item = {"subject": "col1"} - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_object_with_attribute(self): - """Test _get_filter_layer_scope with object having layerFilterScope - attribute.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - from unittest.mock import Mock - - filter_item = Mock() - filter_item.layerFilterScope = [1, 2] - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result == [1, 2] - - def test_get_filter_layer_scope_object_without_attribute(self): - """Test _get_filter_layer_scope with object missing layerFilterScope - attribute.""" - filter_item = Mock() - del filter_item.layerFilterScope - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_non_dict_non_object(self): - """Test _get_filter_layer_scope with non-dict, non-object types.""" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - # Test with string - result = test_viz._get_filter_layer_scope("string_filter") - assert result is None - - # Test with number - result = test_viz._get_filter_layer_scope(123) - assert result is None - - # Test with None - result = test_viz._get_filter_layer_scope(None) - assert result is None - - -class TestDeckGLMultiLayer(SupersetTestCase): - def test_filter_items_by_scope_with_filter_id(self): - """Test _filter_items_by_scope method with items having filterId.""" - datasource = self.get_datasource_mock() - form_data = {} - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - filter_item_1 = Mock() - filter_item_1.filterId = "filter_1" - filter_item_2 = Mock() - filter_item_2.filterId = "filter_2" - filter_item_3 = Mock() - filter_item_3.filterId = "filter_3" - - items = [filter_item_1, filter_item_2, filter_item_3] - layer_index = 0 - layer_filter_scope = {"filter_1": [0, 1], "filter_2": [1], "filter_3": []} - - result = test_viz._filter_items_by_scope(items, layer_index, layer_filter_scope) - - assert len(result) == 2 - assert filter_item_1 in result - assert filter_item_3 in result - assert filter_item_2 not in result - - def test_filter_items_by_scope_without_filter_id(self): - """Test _filter_items_by_scope method with items without filterId.""" - datasource = self.get_datasource_mock() - form_data = {} - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - filter_item_1 = Mock() - del filter_item_1.filterId - filter_item_2 = Mock() - filter_item_2.filterId = None - - items = [filter_item_1, filter_item_2] - layer_index = 0 - layer_filter_scope = {"filter_1": [1]} - - result = test_viz._filter_items_by_scope(items, layer_index, layer_filter_scope) - - assert len(result) == 2 - assert filter_item_1 in result - assert filter_item_2 in result - - def test_process_extra_form_data_filters(self): - """Test _process_extra_form_data_filters method.""" - datasource = self.get_datasource_mock() - form_data = {} - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - layer_index = 0 - layer_filter_scope = {"filter_1": [0, 1], "filter_2": [1], "filter_3": []} - filter_data_mapping = { - "filter_1": [{"column": "col1", "op": "==", "val": "value1"}], - "filter_2": [{"column": "col2", "op": "!=", "val": "value2"}], - "filter_3": [{"column": "col3", "op": ">", "val": 100}], - } - extra_form_data = {"existing_key": "existing_value"} - - result = test_viz._process_extra_form_data_filters( - layer_index, layer_filter_scope, filter_data_mapping, extra_form_data - ) - - expected_filters = [ - {"column": "col1", "op": "==", "val": "value1"}, - {"column": "col3", "op": ">", "val": 100}, - ] - assert result["filters"] == expected_filters - assert result["existing_key"] == "existing_value" - - def test_process_extra_form_data_filters_empty_inputs(self): - """Test _process_extra_form_data_filters with empty inputs.""" - datasource = self.get_datasource_mock() - form_data = {} - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - result = test_viz._process_extra_form_data_filters(0, {}, {}, {}) - assert result == {} - - extra_form_data = {"key": "value"} - result = test_viz._process_extra_form_data_filters(0, {}, {}, extra_form_data) - assert result == extra_form_data - - def test_apply_layer_filtering_without_layer_filter_scope(self): - """Test _apply_layer_filtering when layer_filter_scope is empty.""" - datasource = self.get_datasource_mock() - form_data = { - "extra_filters": [Mock(), Mock()], - "adhoc_filters": [Mock()], - "extra_form_data": {"key": "value"}, - } - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - layer_form_data = {"viz_type": "deck_scatter"} - layer_index = 0 - - result = test_viz._apply_layer_filtering(layer_form_data, layer_index) - - assert result["extra_filters"] == form_data["extra_filters"] - assert result["adhoc_filters"] == form_data["adhoc_filters"] - assert result["extra_form_data"] == form_data["extra_form_data"] - - def test_apply_layer_filtering_with_layer_filter_scope(self): - """Test _apply_layer_filtering with layer_filter_scope.""" - datasource = self.get_datasource_mock() - - extra_filter_1 = Mock() - extra_filter_1.filterId = "filter_1" - extra_filter_2 = Mock() - extra_filter_2.filterId = "filter_2" - - adhoc_filter_1 = Mock() - adhoc_filter_1.filterId = "filter_1" - - form_data = { - "layer_filter_scope": {"filter_1": [0], "filter_2": [1]}, - "filter_data_mapping": { - "filter_1": [{"column": "col1", "op": "==", "val": "value1"}] - }, - "extra_filters": [extra_filter_1, extra_filter_2], - "adhoc_filters": [adhoc_filter_1], - "extra_form_data": {"existing": "data"}, - } - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - layer_form_data = {"viz_type": "deck_scatter"} - layer_index = 0 - - result = test_viz._apply_layer_filtering(layer_form_data, layer_index) - - assert len(result["extra_filters"]) == 1 - assert result["extra_filters"][0].filterId == "filter_1" - assert len(result["adhoc_filters"]) == 1 - assert result["adhoc_filters"][0].filterId == "filter_1" - assert result["extra_form_data"]["filters"] == [ - {"column": "col1", "op": "==", "val": "value1"} - ] - - @with_config({"MAPBOX_API_KEY": "test_key"}) - @patch("superset.viz.viz_types") - @patch("superset.db.session") - def test_get_data_with_layer_filtering(self, mock_db_session, mock_viz_types): - """Test get_data method with layer filtering enabled.""" - datasource = self.get_datasource_mock() - - slice_1 = Mock() - slice_1.form_data = {"viz_type": "deck_scatter", "layer_name": "Layer 1"} - slice_1.data = {"features": [{"type": "Feature"}]} - slice_1.datasource = datasource - - slice_2 = Mock() - slice_2.form_data = {"viz_type": "deck_path", "layer_name": "Layer 2"} - slice_2.data = {"features": [{"type": "Feature"}]} - slice_2.datasource = datasource - - # Mock the database query to return our slice objects - mock_db_session.query.return_value.filter.return_value.all.return_value = [ - slice_1, - slice_2, - ] - - mock_scatter_viz_class = Mock() - mock_scatter_viz_instance = Mock() - mock_scatter_viz_instance.get_payload.return_value = { - "data": {"features": [{"id": 1}]} - } - mock_scatter_viz_class.return_value = mock_scatter_viz_instance - - mock_path_viz_class = Mock() - mock_path_viz_instance = Mock() - mock_path_viz_instance.get_payload.return_value = { - "data": {"features": [{"id": 2}]} - } - mock_path_viz_class.return_value = mock_path_viz_instance - - mock_viz_types.get.side_effect = lambda viz_type: { - "deck_scatter": mock_scatter_viz_class, - "deck_path": mock_path_viz_class, - }.get(viz_type) - - form_data = { - "layer_filter_scope": {"filter_1": [0], "filter_2": [1]}, - "filter_data_mapping": { - "filter_1": [{"column": "col1", "op": "==", "val": "value1"}], - "filter_2": [{"column": "col2", "op": "!=", "val": "value2"}], - }, - "deck_slices": [1, 2], # Use integer IDs instead of Mock objects - } - - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - - test_viz._apply_layer_filtering = Mock( - side_effect=lambda form_data, idx: form_data - ) - - result = test_viz.get_data(pd.DataFrame()) - - assert test_viz._apply_layer_filtering.call_count == 2 - test_viz._apply_layer_filtering.assert_any_call(slice_1.form_data, 0) - test_viz._apply_layer_filtering.assert_any_call(slice_2.form_data, 1) - - assert isinstance(result, dict) - assert "features" in result - assert "mapboxApiKey" in result - assert "slices" in result - assert result["mapboxApiKey"] == "test_key" - - @with_config({"MAPBOX_API_KEY": "test_key"}) - @patch("superset.viz.viz_types") - @patch("superset.db.session") - def test_get_data_filters_none_data_slices(self, mock_db_session, mock_viz_types): - """Test get_data method filters out slices with None data.""" - datasource = self.get_datasource_mock() - - slice_1 = Mock() - slice_1.form_data = {"viz_type": "deck_scatter"} - slice_1.data = {"features": [{"type": "Feature"}]} - slice_1.datasource = datasource - - slice_2 = Mock() - slice_2.form_data = {"viz_type": "deck_path"} - slice_2.data = None - slice_2.datasource = datasource - - # Mock the database query to return our slice objects - mock_db_session.query.return_value.filter.return_value.all.return_value = [ - slice_1, - slice_2, - ] - - mock_viz_class = Mock() - mock_viz_instance = Mock() - mock_viz_instance.get_payload.return_value = {"data": {"features": []}} - mock_viz_class.return_value = mock_viz_instance - mock_viz_types.get.return_value = mock_viz_class - - form_data = {"deck_slices": [1, 2]} # Use integer IDs instead of Mock objects - - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - result = test_viz.get_data(pd.DataFrame()) - - assert isinstance(result, dict) - assert len(result["slices"]) == 1 - assert result["slices"][0] == slice_1.data - - @with_config({"MAPBOX_API_KEY": "test_key"}) - @patch("superset.viz.viz_types") - @patch("superset.db.session") - def test_get_payload_includes_subslice_filter_metadata( - self, - mock_db_session, - mock_viz_types, - ): - """Test deck.gl multi-layer payload includes child filter metadata.""" - datasource = self.get_datasource_mock() - - slice_1 = Mock() - slice_1.form_data = {"viz_type": "deck_scatter"} - slice_1.data = {"features": [{"type": "Feature"}]} - slice_1.datasource = datasource - - slice_2 = Mock() - slice_2.form_data = {"viz_type": "deck_path"} - slice_2.data = {"features": [{"type": "Feature"}]} - slice_2.datasource = datasource - - mock_db_session.query.return_value.filter.return_value.all.return_value = [ - slice_1, - slice_2, - ] - - mock_scatter_viz_class = Mock() - mock_scatter_viz_instance = Mock() - mock_scatter_viz_instance.get_payload.return_value = { - "data": {"features": [{"id": 1}]}, - "applied_filters": [{"column": "Latitude"}], - "rejected_filters": [], - } - mock_scatter_viz_class.return_value = mock_scatter_viz_instance - - mock_path_viz_class = Mock() - mock_path_viz_instance = Mock() - mock_path_viz_instance.get_payload.return_value = { - "data": {"features": [{"id": 2}]}, - "applied_filters": [ - {"column": "Latitude"}, - {"column": "Longitude"}, - ], - "rejected_filters": [ - { - "column": "Country", - "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, - }, - ], - } - mock_path_viz_class.return_value = mock_path_viz_instance - - mock_viz_types.get.side_effect = lambda viz_type: { - "deck_scatter": mock_scatter_viz_class, - "deck_path": mock_path_viz_class, - }.get(viz_type) - - test_viz = viz.DeckGLMultiLayer(datasource, {"deck_slices": [1, 2]}) - test_viz.get_df_payload = Mock(return_value={"df": pd.DataFrame()}) - - result = test_viz.get_payload() - - assert result["applied_filters"] == [ - {"column": "Latitude"}, - {"column": "Longitude"}, - ] - assert result["rejected_filters"] == [ - { - "column": "Country", - "reason": ExtraFiltersReasonType.COL_NOT_IN_DATASOURCE, - }, - ] - - @with_config({"MAPBOX_API_KEY": "test_key"}) - def test_get_data_empty_deck_slices(self): - """Test get_data method with empty deck_slices.""" - datasource = self.get_datasource_mock() - form_data = {"deck_slices": []} - - test_viz = viz.DeckGLMultiLayer(datasource, form_data) - result = test_viz.get_data(pd.DataFrame()) - - assert isinstance(result, dict) - assert result["features"] == {} - assert result["slices"] == [] - assert result["mapboxApiKey"] == "test_key" - - def test_get_base_properties_no_metric(self): - """Test _get_base_properties when metric_label is None""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric_label = None - - data = {"spatial": [10.5, 20.3], "other_field": "value"} - - result = test_viz._get_base_properties(data) - - assert result["position"] == [10.5, 20.3] - assert result["weight"] == 1 - assert len(result) == 2 - - def test_setup_metric_label(self): - """Test _setup_metric_label with metric""" - form_data = {"size": "test_metric"} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric = Mock() - test_viz.metric.value = "test_metric" - - with patch("superset.viz.utils.get_metric_name") as mock_get_name: - mock_get_name.return_value = "test_metric" - test_viz._setup_metric_label() - - assert test_viz.metric_label == "test_metric" - mock_get_name.assert_called_once_with(test_viz.metric) - - def test_setup_metric_label_no_metric(self): - """Test _setup_metric_label without metric""" - form_data = {} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - test_viz.metric = None - - test_viz._setup_metric_label() - - assert test_viz.metric_label is None - - def test_should_apply_layer_filtering_true(self): - """Test _should_apply_layer_filtering returns True when conditions are met""" - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is True - - def test_should_apply_layer_filtering_false_missing_slice_id(self): - """Test _should_apply_layer_filtering returns False when slice_id is missing""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is False - - def test_should_apply_layer_filtering_false_missing_adhoc_filters(self): - """Test _should_apply_layer_filtering returns False when - adhoc_filters missing""" - form_data = {"slice_id": 123} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is False - - def test_should_apply_layer_filtering_false_no_layer_scoped_filters(self): - """Test _should_apply_layer_filtering returns False when no layer - scoped filters""" - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._should_apply_layer_filtering(form_data) - - assert result is False - - def test_has_layer_scoped_filters_true_with_dict(self): - """Test _has_layer_scoped_filters returns True when filter - has layerFilterScope""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is True - - def test_has_layer_scoped_filters_true_with_non_none_value(self): - """Test _has_layer_scoped_filters returns True when layerFilterScope not None""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is True - - def test_has_layer_scoped_filters_false_none_value(self): - """Test _has_layer_scoped_filters returns False when layerFilterScope is None""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": None, - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is False - - def test_has_layer_scoped_filters_false_no_scoped_filters(self): - """Test _has_layer_scoped_filters returns False when no filters have - layerFilterScope""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._has_layer_scoped_filters(form_data) - - assert result is False - - def test_apply_multilayer_filtering_filters_by_layer_scope(self): - """Test _apply_multilayer_filtering filters adhoc_filters by layer scope""" - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0, 1], - "deck_slices": [123, 456], - }, - { - "clause": "WHERE", - "subject": "col2", - "operator": "!=", - "comparator": "value2", - "layerFilterScope": [1], - "deck_slices": [123, 456], - }, - { - "clause": "WHERE", - "subject": "col3", - "operator": ">", - "comparator": "value3", - "layerFilterScope": [2], - "deck_slices": [123, 456], - }, - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._apply_multilayer_filtering(form_data) - - assert len(result["adhoc_filters"]) == 1 - assert result["adhoc_filters"][0]["subject"] == "col1" - - def test_apply_multilayer_filtering_no_deck_slices(self): - """Test _apply_multilayer_filtering when no deck_slices in filters""" - form_data = { - "slice_id": 123, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._apply_multilayer_filtering(form_data) - - assert len(result["adhoc_filters"]) == 1 - assert result["adhoc_filters"][0]["subject"] == "col1" - - def test_apply_multilayer_filtering_slice_not_in_deck_slices(self): - """Test _apply_multilayer_filtering when slice_id not in deck_slices""" - form_data = { - "slice_id": 999, - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "layerFilterScope": [0], - "deck_slices": [123, 456], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._apply_multilayer_filtering(form_data) - - # Should return original form_data unchanged when slice_id not in deck_slices - assert result == form_data - - def test_get_deck_slices_from_filters_found(self): - """Test _get_deck_slices_from_filters when deck_slices found""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - "deck_slices": [123, 456], - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result == [123, 456] - - def test_get_deck_slices_from_filters_not_found(self): - """Test _get_deck_slices_from_filters when no deck_slices found""" - form_data = { - "adhoc_filters": [ - { - "clause": "WHERE", - "subject": "col1", - "operator": "==", - "comparator": "value1", - } - ], - } - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result is None - - def test_get_deck_slices_from_filters_empty_filters(self): - """Test _get_deck_slices_from_filters with empty adhoc_filters""" - form_data = {"adhoc_filters": []} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, form_data) - - result = test_viz._get_deck_slices_from_filters(form_data) - - assert result is None - - def test_get_filter_layer_scope_dict(self): - """Test _get_filter_layer_scope with dict filter""" - filter_item = {"layerFilterScope": [0, 1]} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result == [0, 1] - - def test_get_filter_layer_scope_dict_none(self): - """Test _get_filter_layer_scope with dict filter having None layerFilterScope""" - filter_item = {"layerFilterScope": None} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_dict_missing_key(self): - """Test _get_filter_layer_scope with dict filter missing layerFilterScope key""" - filter_item = {"other_key": "value"} - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_object_with_attribute(self): - """Test _get_filter_layer_scope with object having layerFilterScope attribute""" - filter_item = Mock() - filter_item.layerFilterScope = [0, 1] - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result == [0, 1] - - def test_get_filter_layer_scope_object_without_attribute(self): - """Test _get_filter_layer_scope with object missing - layerFilterScope attribute""" - filter_item = Mock() - del filter_item.layerFilterScope - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - def test_get_filter_layer_scope_non_dict_non_object(self): - """Test _get_filter_layer_scope with non-dict, non-object filter""" - filter_item = "string_filter" - datasource = self.get_datasource_mock() - test_viz = viz.BaseDeckGLViz(datasource, {}) - - result = test_viz._get_filter_layer_scope(filter_item) - - assert result is None - - -class TestTimeSeriesViz(SupersetTestCase): - def test_timeseries_unicode_data(self): - datasource = self.get_datasource_mock() - form_data = {"groupby": ["name"], "metrics": ["sum__payout"]} - raw = {} - raw["name"] = [ - "Real Madrid C.F.🇺🇸🇬🇧", - "Real Madrid C.F.🇺🇸🇬🇧", - "Real Madrid Basket", - "Real Madrid Basket", - ] - raw["__timestamp"] = [ - "2018-02-20T00:00:00", - "2018-03-09T00:00:00", - "2018-02-20T00:00:00", - "2018-03-09T00:00:00", - ] - raw["sum__payout"] = [2, 2, 4, 4] - df = pd.DataFrame(raw) - - test_viz = viz.NVD3TimeSeriesViz(datasource, form_data) - viz_data = {} - viz_data = test_viz.get_data(df) - expected = [ - { - "values": [ - {"y": 4, "x": "2018-02-20T00:00:00"}, - {"y": 4, "x": "2018-03-09T00:00:00"}, - ], - "key": ("Real Madrid Basket",), - }, - { - "values": [ - {"y": 2, "x": "2018-02-20T00:00:00"}, - {"y": 2, "x": "2018-03-09T00:00:00"}, - ], - "key": ("Real Madrid C.F.\U0001f1fa\U0001f1f8\U0001f1ec\U0001f1e7",), - }, - ] - assert expected == viz_data - - def test_process_data_resample(self): - datasource = self.get_datasource_mock() - - df = pd.DataFrame( - { - "__timestamp": pd.to_datetime( - ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"] - ), - "y": [1.0, 2.0, 5.0, 7.0], - } - ) - - assert viz.NVD3TimeSeriesViz( - datasource, - {"metrics": ["y"], "resample_method": "sum", "resample_rule": "1D"}, - ).process_data(df)["y"].tolist() == [1.0, 2.0, 0.0, 0.0, 5.0, 0.0, 7.0] - - np.testing.assert_equal( - viz.NVD3TimeSeriesViz( - datasource, - {"metrics": ["y"], "resample_method": "asfreq", "resample_rule": "1D"}, - ) - .process_data(df)["y"] - .tolist(), - [1.0, 2.0, np.nan, np.nan, 5.0, np.nan, 7.0], - ) - - def test_apply_rolling(self): - datasource = self.get_datasource_mock() - df = pd.DataFrame( - index=pd.to_datetime( - ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"] - ), - data={"y": [1.0, 2.0, 3.0, 4.0]}, - ) - assert viz.NVD3TimeSeriesViz( - datasource, - { - "metrics": ["y"], - "rolling_type": "cumsum", - "rolling_periods": 0, - "min_periods": 0, - }, - ).apply_rolling(df)["y"].tolist() == [1.0, 3.0, 6.0, 10.0] - assert viz.NVD3TimeSeriesViz( - datasource, - { - "metrics": ["y"], - "rolling_type": "sum", - "rolling_periods": 2, - "min_periods": 0, - }, - ).apply_rolling(df)["y"].tolist() == [1.0, 3.0, 5.0, 7.0] - assert viz.NVD3TimeSeriesViz( - datasource, - { - "metrics": ["y"], - "rolling_type": "mean", - "rolling_periods": 10, - "min_periods": 0, - }, - ).apply_rolling(df)["y"].tolist() == [1.0, 1.5, 2.0, 2.5] - - def test_apply_rolling_without_data(self): - datasource = self.get_datasource_mock() - df = pd.DataFrame( - index=pd.to_datetime( - ["2019-01-01", "2019-01-02", "2019-01-05", "2019-01-07"] - ), - data={"y": [1.0, 2.0, 3.0, 4.0]}, - ) - test_viz = viz.NVD3TimeSeriesViz( - datasource, - { - "metrics": ["y"], - "rolling_type": "cumsum", - "rolling_periods": 4, - "min_periods": 4, - }, - ) - with pytest.raises(QueryObjectValidationError): - test_viz.apply_rolling(df) diff --git a/tests/unit_tests/async_events/async_query_manager_tests.py b/tests/unit_tests/async_events/async_query_manager_tests.py index f5b5c97c8a21..1c9498bb8cad 100644 --- a/tests/unit_tests/async_events/async_query_manager_tests.py +++ b/tests/unit_tests/async_events/async_query_manager_tests.py @@ -364,57 +364,3 @@ def test_view(): mock_request.cookies = {JWT_TOKEN_COOKIE_NAME: token} channel = async_query_manager.parse_channel_id_from_request(mock_request) assert channel # valid UUID string - - -@mark.parametrize( - "cache_type, cache_backend", - [ - ("RedisCacheBackend", mock.Mock(spec=RedisCacheBackend)), - ("RedisSentinelCacheBackend", mock.Mock(spec=RedisSentinelCacheBackend)), - ], -) -@mock.patch("superset.is_feature_enabled") -def test_submit_explore_json_job_as_guest_user( - is_feature_enabled_mock, async_query_manager, cache_type, cache_backend -): - is_feature_enabled_mock.return_value = True - set_current_as_guest_user() - - # Mock the get_cache_backend method to return the current cache backend - async_query_manager.get_cache_backend = mock.Mock(return_value=cache_backend) - - job_mock = Mock() - async_query_manager._load_explore_json_into_cache_job = job_mock - job_meta = async_query_manager.submit_explore_json_job( - channel_id="test_channel_id", - form_data={}, - response_type="json", - ) - - job_mock.apply_async.assert_called_once_with( - args=[ - { - "channel_id": "test_channel_id", - "errors": [], - "guest_token": { - "user": {}, - "resources": [{"type": "dashboard", "id": "some-uuid"}], - "rls_rules": [{"clause": '"STATEID" = 3'}], - "iat": 1700000000.0, - "exp": 1700000300.0, - "aud": "http://0.0.0.0:8080/", - "type": "guest", - }, - "job_id": ANY, - "result_url": None, - "status": "pending", - "user_id": None, - }, - {}, - "json", - False, - ], - expires=3600, - ) - - assert "guest_token" not in job_meta diff --git a/tests/unit_tests/commands/chart/warm_up_cache_test.py b/tests/unit_tests/commands/chart/warm_up_cache_test.py index f178ab85e753..5f97e9b3fa2f 100644 --- a/tests/unit_tests/commands/chart/warm_up_cache_test.py +++ b/tests/unit_tests/commands/chart/warm_up_cache_test.py @@ -57,33 +57,29 @@ def test_applies_dashboard_filters_to_non_legacy_chart( # Mock dependencies with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "echarts_timeseries_bar"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [{"error": None, "status": "success"}] - } + mock_chart_data_command.return_value.run.return_value = { + "queries": [{"error": None, "status": "success"}] + } - # Execute with dashboard_id - result = ChartWarmUpCacheCommand(chart, 42, None).run() + # Execute with dashboard_id + result = ChartWarmUpCacheCommand(chart, 42, None).run() - # VALIDATE: Filters were added to query.filter - assert len(mock_query.filter) == 1, "Filter should be added to query" - assert mock_query.filter[0] == { - "col": "country", - "op": "in", - "val": ["USA", "France"], - }, "Filter content should match dashboard filter" + # VALIDATE: Filters were added to query.filter + assert len(mock_query.filter) == 1, "Filter should be added to query" + assert mock_query.filter[0] == { + "col": "country", + "op": "in", + "val": ["USA", "France"], + }, "Filter content should match dashboard filter" - # VALIDATE: get_dashboard_extra_filters was called correctly - mock_get_dashboard_filters.assert_called_once_with(123, 42) + # VALIDATE: get_dashboard_extra_filters was called correctly + mock_get_dashboard_filters.assert_called_once_with(123, 42) - # VALIDATE: force flag was set - assert mock_qc.force is True, "force should be set to True" + # VALIDATE: force flag was set + assert mock_qc.force is True, "force should be set to True" - # VALIDATE: Result is correct - assert result["chart_id"] == 123 + # VALIDATE: Result is correct + assert result["chart_id"] == 123 @patch("superset.commands.chart.warm_up_cache.ChartDataCommand") @@ -104,22 +100,18 @@ def test_no_filters_applied_without_dashboard_id(mock_chart_data_command): mock_qc.queries = [mock_query] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "big_number"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [{"error": None, "status": "success"}] - } + mock_chart_data_command.return_value.run.return_value = { + "queries": [{"error": None, "status": "success"}] + } - # Execute WITHOUT dashboard_id - ChartWarmUpCacheCommand(chart, None, None).run() + # Execute WITHOUT dashboard_id + ChartWarmUpCacheCommand(chart, None, None).run() - # VALIDATE: Filter list unchanged (no dashboard filters added) - assert len(mock_query.filter) == 1, "Filter count should remain the same" - assert mock_query.filter == [ - {"col": "existing", "op": "==", "val": "filter"} - ], "Existing filters should be unchanged" + # VALIDATE: Filter list unchanged (no dashboard filters added) + assert len(mock_query.filter) == 1, "Filter count should remain the same" + assert mock_query.filter == [ + {"col": "existing", "op": "==", "val": "filter"} + ], "Existing filters should be unchanged" @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters") @@ -142,24 +134,20 @@ def test_extra_filters_parameter_takes_precedence( mock_qc.queries = [mock_query] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "pie"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [{"error": None, "status": "success"}] - } + mock_chart_data_command.return_value.run.return_value = { + "queries": [{"error": None, "status": "success"}] + } - # Execute with extra_filters parameter - extra_filters_json = '[{"col": "state", "op": "==", "val": "CA"}]' - ChartWarmUpCacheCommand(chart, 42, extra_filters_json).run() + # Execute with extra_filters parameter + extra_filters_json = '[{"col": "state", "op": "==", "val": "CA"}]' + ChartWarmUpCacheCommand(chart, 42, extra_filters_json).run() - # VALIDATE: get_dashboard_extra_filters should NOT be called - mock_get_dashboard_filters.assert_not_called() + # VALIDATE: get_dashboard_extra_filters should NOT be called + mock_get_dashboard_filters.assert_not_called() - # VALIDATE: extra_filters were parsed and applied - assert len(mock_query.filter) == 1 - assert mock_query.filter[0] == {"col": "state", "op": "==", "val": "CA"} + # VALIDATE: extra_filters were parsed and applied + assert len(mock_query.filter) == 1 + assert mock_query.filter[0] == {"col": "state", "op": "==", "val": "CA"} @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters") @@ -189,24 +177,20 @@ def test_handles_multiple_queries_in_query_context( mock_qc.queries = [mock_query1, mock_query2] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "heatmap_v2"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [ - {"error": None, "status": "success"}, - {"error": None, "status": "success"}, - ] - } - - ChartWarmUpCacheCommand(chart, 42, None).run() - - # VALIDATE: Filters added to BOTH queries - assert len(mock_query1.filter) == 1, "Filter should be added to query 1" - assert len(mock_query2.filter) == 1, "Filter should be added to query 2" - assert mock_query1.filter[0]["col"] == "country" - assert mock_query2.filter[0]["col"] == "country" + mock_chart_data_command.return_value.run.return_value = { + "queries": [ + {"error": None, "status": "success"}, + {"error": None, "status": "success"}, + ] + } + + ChartWarmUpCacheCommand(chart, 42, None).run() + + # VALIDATE: Filters added to BOTH queries + assert len(mock_query1.filter) == 1, "Filter should be added to query 1" + assert len(mock_query2.filter) == 1, "Filter should be added to query 2" + assert mock_query1.filter[0]["col"] == "country" + assert mock_query2.filter[0]["col"] == "country" @patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters") @@ -232,23 +216,19 @@ def test_handles_empty_dashboard_filters( mock_qc.queries = [mock_query] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "echarts_area"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [{"error": None, "status": "success"}] - } + mock_chart_data_command.return_value.run.return_value = { + "queries": [{"error": None, "status": "success"}] + } - ChartWarmUpCacheCommand(chart, 42, None).run() + ChartWarmUpCacheCommand(chart, 42, None).run() - # VALIDATE: No filters added (empty list case) - assert len(mock_query.filter) == 0, ( - "No filters should be added when dashboard has no filters" - ) - assert mock_get_dashboard_filters.called, ( - "Should still call get_dashboard_extra_filters" - ) + # VALIDATE: No filters added (empty list case) + assert len(mock_query.filter) == 0, ( + "No filters should be added when dashboard has no filters" + ) + assert mock_get_dashboard_filters.called, ( + "Should still call get_dashboard_extra_filters" + ) @patch("superset.commands.chart.warm_up_cache.ChartDataCommand") @@ -271,22 +251,18 @@ def test_invalid_json_in_extra_filters_raises_error(mock_chart_data_command): mock_qc.queries = [mock_query] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "pie"}], - ): - result = ChartWarmUpCacheCommand(chart, 42, invalid_json).run() - - assert result["viz_error"] is not None, "Should return an error" - assert result["chart_id"] == 128 - # JSONDecodeError messages vary across Python versions - error_str = str(result["viz_error"]).lower() - assert ( - "json" in error_str - or "decode" in error_str - or "expecting" in error_str - or "delimiter" in error_str - ), f"Error should be a JSON decode issue: {result['viz_error']}" + result = ChartWarmUpCacheCommand(chart, 42, invalid_json).run() + + assert result["viz_error"] is not None, "Should return an error" + assert result["chart_id"] == 128 + # JSONDecodeError messages vary across Python versions + error_str = str(result["viz_error"]).lower() + assert ( + "json" in error_str + or "decode" in error_str + or "expecting" in error_str + or "delimiter" in error_str + ), f"Error should be a JSON decode issue: {result['viz_error']}" @patch("superset.commands.chart.warm_up_cache.ChartDataCommand") @@ -302,135 +278,17 @@ def test_none_query_context_raises_chart_invalid_error(mock_chart_data_command): # Mock get_query_context to return None (chart has no query_context) with patch.object(chart, "get_query_context", return_value=None): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "echarts_timeseries"}], - ): - result = ChartWarmUpCacheCommand(chart, None, None).run() - - assert result["viz_error"] is not None, "Should return an error" - assert result["chart_id"] == 129 - error_str = str(result["viz_error"]).lower() - assert "query context" in error_str, ( - f"Error should mention query context: {result['viz_error']}" - ) - assert "not exist" in error_str, ( - f"Error should mention not exist: {result['viz_error']}" - ) - - -@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"]) -def test_legacy_chart_without_datasource_raises_error(): - """Verify that legacy chart without datasource raises ChartInvalidError""" - chart = Slice( - id=130, - slice_name="Legacy Chart", - viz_type="table", - datasource_id=None, - datasource_type=None, - ) - - with patch.object( - type(chart), "datasource", new_callable=lambda: property(lambda self: None) - ): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "table"}], - ): - result = ChartWarmUpCacheCommand(chart, None, None).run() - - assert result["viz_error"] is not None, "Should return an error" - assert result["chart_id"] == 130 - error_str = str(result["viz_error"]).lower() - assert "datasource" in error_str, ( - f"Error should mention datasource: {result['viz_error']}" - ) - assert "not exist" in error_str, ( - f"Error should mention not exist: {result['viz_error']}" - ) - - -@patch("superset.commands.chart.warm_up_cache.get_dashboard_extra_filters") -@patch("superset.commands.chart.warm_up_cache.get_viz") -@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"]) -@patch("superset.commands.chart.warm_up_cache.g") -def test_legacy_chart_warm_up_with_dashboard( - mock_g, mock_get_viz, mock_get_dashboard_filters -): - """Test successful legacy chart warm-up with dashboard filters""" - mock_get_dashboard_filters.return_value = [ - {"col": "country", "op": "==", "val": "USA"} - ] - - chart = Slice( - id=131, - slice_name="Legacy Table", - viz_type="table", - datasource_id=1, - datasource_type="table", - ) - - mock_datasource = Mock() - mock_datasource.type = "table" - mock_datasource.id = 1 - - mock_viz = Mock() - mock_viz.get_payload.return_value = {"errors": None, "status": "success"} - mock_get_viz.return_value = mock_viz - - with patch.object( - type(chart), - "datasource", - new_callable=lambda: property(lambda self: mock_datasource), - ): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "table"}], - ): - result = ChartWarmUpCacheCommand(chart, 42, None).run() - - assert result["chart_id"] == 131 - assert result["viz_error"] is None - assert result["viz_status"] == "success" - assert hasattr(mock_g, "form_data") is False - mock_get_dashboard_filters.assert_called_once_with(131, 42) - - -@patch("superset.commands.chart.warm_up_cache.get_viz") -@patch("superset.commands.chart.warm_up_cache.viz_types", ["table"]) -@patch("superset.commands.chart.warm_up_cache.g") -def test_legacy_chart_warm_up_without_dashboard(mock_g, mock_get_viz): - """Test successful legacy chart warm-up without dashboard""" - chart = Slice( - id=134, - slice_name="Legacy Table", - viz_type="table", - datasource_id=1, - datasource_type="table", - ) - - mock_datasource = Mock() - mock_datasource.type = "table" - mock_datasource.id = 1 - - mock_viz = Mock() - mock_viz.get_payload.return_value = {"errors": None, "status": "success"} - mock_get_viz.return_value = mock_viz - - with patch.object( - type(chart), - "datasource", - new_callable=lambda: property(lambda self: mock_datasource), - ): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "table"}], - ): - result = ChartWarmUpCacheCommand(chart, None, None).run() + result = ChartWarmUpCacheCommand(chart, None, None).run() - assert result["chart_id"] == 134 - assert result["viz_error"] is None - assert result["viz_status"] == "success" + assert result["viz_error"] is not None, "Should return an error" + assert result["chart_id"] == 129 + error_str = str(result["viz_error"]).lower() + assert "query context" in error_str, ( + f"Error should mention query context: {result['viz_error']}" + ) + assert "not exist" in error_str, ( + f"Error should mention not exist: {result['viz_error']}" + ) @patch("superset.commands.chart.warm_up_cache.ChartDataCommand") @@ -450,22 +308,18 @@ def test_non_legacy_chart_returns_first_error(mock_chart_data_command): mock_qc.queries = [mock_query] with patch.object(chart, "get_query_context", return_value=mock_qc): - with patch( - "superset.commands.chart.warm_up_cache.get_form_data", - return_value=[{"viz_type": "echarts_timeseries"}], - ): - mock_chart_data_command.return_value.run.return_value = { - "queries": [ - {"error": "Database connection failed", "status": "failed"}, - {"error": None, "status": "success"}, - ] - } - - result = ChartWarmUpCacheCommand(chart, None, None).run() - - assert result["chart_id"] == 132 - assert result["viz_error"] == "Database connection failed" - assert result["viz_status"] == "failed" + mock_chart_data_command.return_value.run.return_value = { + "queries": [ + {"error": "Database connection failed", "status": "failed"}, + {"error": None, "status": "success"}, + ] + } + + result = ChartWarmUpCacheCommand(chart, None, None).run() + + assert result["chart_id"] == 132 + assert result["viz_error"] == "Database connection failed" + assert result["viz_status"] == "failed" @patch("superset.commands.chart.warm_up_cache.db") diff --git a/tests/unit_tests/middleware/test_legacy_prefix_redirect.py b/tests/unit_tests/middleware/test_legacy_prefix_redirect.py index 4b2011834311..d966b1105543 100644 --- a/tests/unit_tests/middleware/test_legacy_prefix_redirect.py +++ b/tests/unit_tests/middleware/test_legacy_prefix_redirect.py @@ -84,9 +84,6 @@ def _build_client(app_root: str = "/") -> Client: ("/superset/explore/", "/explore/"), ("/superset/explore/table/5/", "/explore/table/5/"), ("/superset/explore/p/key123/", "/explore/p/key123/"), - ("/superset/explore_json/", "/explore_json/"), - ("/superset/explore_json/table/5/", "/explore_json/table/5/"), - ("/superset/explore_json/data/cache_abc/", "/explore_json/data/cache_abc/"), ("/superset/tags/", "/tags/"), ("/superset/all_entities/", "/all_entities/"), ] @@ -95,8 +92,6 @@ def _build_client(app_root: str = "/") -> Client: _POST_BODY_PRESERVED_ROWS: list[tuple[str, str]] = [ ("/superset/explore/", "/explore/"), ("/superset/explore/table/5/", "/explore/table/5/"), - ("/superset/explore_json/", "/explore_json/"), - ("/superset/explore_json/table/5/", "/explore_json/table/5/"), ("/superset/log/", "/log/"), ] @@ -112,7 +107,6 @@ def _build_client(app_root: str = "/") -> Client: "/superset/file-handler", "/superset/sqllab/history/", "/superset/explore/p/key123/", - "/superset/explore_json/data/cache_abc/", "/superset/tags/", "/superset/all_entities/", ] @@ -642,8 +636,6 @@ def test_query_string_preserved_under_subdir() -> None: "/file-handler", "/log/", "/sqllab/history/", - "/explore_json/", - "/explore_json/data/", "/explore/", "/explore/p/", "/tags/", diff --git a/tests/unit_tests/security/manager_test.py b/tests/unit_tests/security/manager_test.py index 13f339aeef41..b940225c6cba 100644 --- a/tests/unit_tests/security/manager_test.py +++ b/tests/unit_tests/security/manager_test.py @@ -953,7 +953,6 @@ def test_raise_for_access_query_default_schema( query=query, query_context=None, table=None, - viz=None, ) is None ) @@ -968,7 +967,6 @@ def test_raise_for_access_query_default_schema( query=query, query_context=None, table=None, - viz=None, ) assert ( str(excinfo.value) @@ -1007,7 +1005,6 @@ def test_raise_for_access_jinja_sql(mocker: MockerFixture, app_context: None) -> query=query, query_context=None, table=None, - viz=None, ) get_table_access_error_object.assert_called_with({Table("ab_user", "public", None)}) diff --git a/tests/unit_tests/tasks/test_async_queries.py b/tests/unit_tests/tasks/test_async_queries.py index c442f3095497..2000627f060a 100644 --- a/tests/unit_tests/tasks/test_async_queries.py +++ b/tests/unit_tests/tasks/test_async_queries.py @@ -23,10 +23,8 @@ from superset.commands.chart.exceptions import ChartDataQueryFailedError from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( - OAuth2RedirectError, SupersetErrorException, SupersetErrorsException, - SupersetVizException, ) @@ -206,170 +204,3 @@ def test_load_chart_data_into_cache_preserves_oauth2_redirect_error( "tab_id": "tab-123", "redirect_uri": "https://superset.example.com/oauth2/redirect", } - - -@mock.patch("superset.tasks.async_queries.security_manager") -@mock.patch("superset.tasks.async_queries.async_query_manager") -@mock.patch("superset.tasks.async_queries.get_viz") -@mock.patch("superset.tasks.async_queries.get_datasource_info") -def test_load_explore_json_into_cache_preserves_oauth2_redirect_error( - mock_get_datasource_info, - mock_get_viz, - mock_async_query_manager, - mock_security_manager, -): - """ - OAuth2RedirectError raised by ``viz_obj.get_payload`` must reach the async - job's errors list as a structured SIP-40 envelope so the frontend can - render the OAuth2 banner identically to the sync legacy path. - """ - from superset.tasks.async_queries import load_explore_json_into_cache - - job_metadata = {"user_id": 1} - form_data: dict = {} - - mock_get_datasource_info.return_value = (1, "table") - mock_security_manager.get_user_by_id.return_value = mock.MagicMock() - mock_async_query_manager.STATUS_ERROR = "error" - - viz_obj = mock.MagicMock() - viz_obj.get_payload.side_effect = OAuth2RedirectError( - url="https://accounts.example.com/o/oauth2/v2/auth?...", - tab_id="tab-123", - redirect_uri="https://superset.example.com/oauth2/redirect", - ) - mock_get_viz.return_value = viz_obj - - with pytest.raises(OAuth2RedirectError): - load_explore_json_into_cache(job_metadata, form_data) - - call_args = mock_async_query_manager.update_job.call_args - assert call_args[0] == (job_metadata, "error") - errors = call_args[1]["errors"] - assert len(errors) == 1 - assert errors[0]["error_type"] == SupersetErrorType.OAUTH2_REDIRECT - assert errors[0]["extra"] == { - "url": "https://accounts.example.com/o/oauth2/v2/auth?...", - "tab_id": "tab-123", - "redirect_uri": "https://superset.example.com/oauth2/redirect", - } - - -@mock.patch("superset.tasks.async_queries.security_manager") -@mock.patch("superset.tasks.async_queries.async_query_manager") -@mock.patch("superset.tasks.async_queries.get_viz") -@mock.patch("superset.tasks.async_queries.get_datasource_info") -def test_load_explore_json_into_cache_preserves_superset_errors_exception( - mock_get_datasource_info, - mock_get_viz, - mock_async_query_manager, - mock_security_manager, -): - """SupersetErrorsException must be preserved as a list of SIP-40 dicts.""" - from superset.tasks.async_queries import load_explore_json_into_cache - - job_metadata = {"user_id": 1} - form_data: dict = {} - - mock_get_datasource_info.return_value = (1, "table") - mock_security_manager.get_user_by_id.return_value = mock.MagicMock() - mock_async_query_manager.STATUS_ERROR = "error" - - viz_obj = mock.MagicMock() - viz_obj.get_payload.side_effect = SupersetErrorsException( - [ - SupersetError( - message="Column not found", - error_type=SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR, - level=ErrorLevel.ERROR, - ), - SupersetError( - message="Table not found", - error_type=SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR, - level=ErrorLevel.WARNING, - ), - ] - ) - mock_get_viz.return_value = viz_obj - - with pytest.raises(SupersetErrorsException): - load_explore_json_into_cache(job_metadata, form_data) - - errors = mock_async_query_manager.update_job.call_args[1]["errors"] - assert len(errors) == 2 - assert errors[0]["error_type"] == SupersetErrorType.COLUMN_DOES_NOT_EXIST_ERROR - assert errors[1]["error_type"] == SupersetErrorType.TABLE_DOES_NOT_EXIST_ERROR - - -@mock.patch("superset.tasks.async_queries.security_manager") -@mock.patch("superset.tasks.async_queries.async_query_manager") -@mock.patch("superset.tasks.async_queries.get_viz") -@mock.patch("superset.tasks.async_queries.get_datasource_info") -def test_load_explore_json_into_cache_preserves_superset_viz_exception( - mock_get_datasource_info, - mock_get_viz, - mock_async_query_manager, - mock_security_manager, -): - """ - Test that SupersetVizException passes ``ex.errors`` straight through. - """ - from superset.tasks.async_queries import load_explore_json_into_cache - - job_metadata = {"user_id": 1} - form_data: dict = {} - - mock_get_datasource_info.return_value = (1, "table") - mock_security_manager.get_user_by_id.return_value = mock.MagicMock() - mock_async_query_manager.STATUS_ERROR = "error" - - payload_errors = [ - { - "message": "Bad column", - "error_type": SupersetErrorType.VIZ_GET_DF_ERROR, - "level": ErrorLevel.ERROR, - } - ] - viz_obj = mock.MagicMock() - viz_obj.get_payload.return_value = {"errors": payload_errors} - viz_obj.has_error.return_value = True - mock_get_viz.return_value = viz_obj - - with pytest.raises(SupersetVizException): - load_explore_json_into_cache(job_metadata, form_data) - - errors = mock_async_query_manager.update_job.call_args[1]["errors"] - assert errors == payload_errors - - -@mock.patch("superset.tasks.async_queries.security_manager") -@mock.patch("superset.tasks.async_queries.async_query_manager") -@mock.patch("superset.tasks.async_queries.get_viz") -@mock.patch("superset.tasks.async_queries.get_datasource_info") -def test_load_explore_json_into_cache_falls_back_to_string_for_generic_exception( - mock_get_datasource_info, - mock_get_viz, - mock_async_query_manager, - mock_security_manager, -): - """ - Test that Non-Superset exception are passed as plain-string error. - """ - from superset.tasks.async_queries import load_explore_json_into_cache - - job_metadata = {"user_id": 1} - form_data: dict = {} - - mock_get_datasource_info.return_value = (1, "table") - mock_security_manager.get_user_by_id.return_value = mock.MagicMock() - mock_async_query_manager.STATUS_ERROR = "error" - - viz_obj = mock.MagicMock() - viz_obj.get_payload.side_effect = RuntimeError("boom") - mock_get_viz.return_value = viz_obj - - with pytest.raises(RuntimeError): - load_explore_json_into_cache(job_metadata, form_data) - - errors = mock_async_query_manager.update_job.call_args[1]["errors"] - assert errors == ["boom"] diff --git a/tests/unit_tests/test_subdirectory_url_for.py b/tests/unit_tests/test_subdirectory_url_for.py index 45fecb0a5b86..e09042a6451d 100644 --- a/tests/unit_tests/test_subdirectory_url_for.py +++ b/tests/unit_tests/test_subdirectory_url_for.py @@ -168,19 +168,6 @@ def test_dashboard_model_url_has_no_route_prefix() -> None: assert Dashboard.get_url(11, "births") == "/dashboard/births/" -def test_slice_explore_json_url_has_no_route_prefix(app_context: None) -> None: - """`Slice.explore_json_url` previously baked `/superset/explore_json` into - the path. After `Superset.route_base = ""` the rule is `/explore_json/`, - so the literal pointed at a non-existent route. Pin the prefix-free shape - so downstream `ensureAppRoot` callers prepend the application root once. - """ - from superset.models.slice import Slice - - slc = Slice(id=42) - assert slc.explore_json_url.startswith("/explore_json/") - assert "/superset/explore_json" not in slc.explore_json_url - - def test_database_sql_url_resolves_to_live_sqllab_route() -> None: """`Database.sql_url` previously returned `/superset/sql//` — a route that was removed when SQL Lab moved to its own blueprint at `/sqllab/`. diff --git a/tests/unit_tests/test_viz_cache_key.py b/tests/unit_tests/test_viz_cache_key.py deleted file mode 100644 index d6eb614265cb..000000000000 --- a/tests/unit_tests/test_viz_cache_key.py +++ /dev/null @@ -1,111 +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. -""" -Behavioral tests for ``viz.BaseViz.cache_key`` covering per-user cache-key -inclusion. -""" - -from typing import Any -from unittest.mock import patch - -from flask_appbuilder.security.sqla.models import User - -from superset import viz -from superset.connectors.sqla.models import SqlaTable -from superset.models.core import Database -from superset.utils.core import override_user - -QUERY_OBJ: dict[str, Any] = {"row_limit": 100, "from_dttm": None, "to_dttm": None} - - -def _viz_for(database: Database) -> viz.BaseViz: - datasource = SqlaTable( - table_name="t", - columns=[], - metrics=[], - main_dttm_col=None, - database=database, - ) - return viz.BaseViz(datasource=datasource, form_data={"viz_type": "table"}) - - -def test_no_per_user_opt_in_keys_match_across_users(): - """ - Without any per-user caching opt-in, two different users on the same - database/query must produce the *same* cache key (regression guard — we - must not accidentally make every cache key per-user). - """ - database = Database(database_name="d", sqlalchemy_uri="sqlite://") - obj = _viz_for(database) - - with override_user(User(username="alice")): - key_a = obj.cache_key(QUERY_OBJ) - with override_user(User(username="bob")): - key_b = obj.cache_key(QUERY_OBJ) - - assert key_a == key_b - - -def test_per_user_caching_in_extra_yields_distinct_keys_per_user(): - """ - With ``per_user_caching: true`` set on the database, two different users - must produce *different* cache keys for the same query. - """ - database = Database( - database_name="d", - sqlalchemy_uri="sqlite://", - extra='{"per_user_caching": true}', - ) - obj = _viz_for(database) - - with override_user(User(username="alice")): - key_a = obj.cache_key(QUERY_OBJ) - with override_user(User(username="bob")): - key_b = obj.cache_key(QUERY_OBJ) - - assert key_a != key_b - - -def test_same_user_same_query_idempotent(): - database = Database( - database_name="d", - sqlalchemy_uri="sqlite://", - extra='{"per_user_caching": true}', - ) - obj = _viz_for(database) - - with override_user(User(username="alice")): - assert obj.cache_key(QUERY_OBJ) == obj.cache_key(QUERY_OBJ) - - -@patch("superset.utils.cache_keys.feature_flag_manager") -def test_cache_query_by_user_flag_yields_distinct_keys(feature_flag_mock): - """ - Global ``CACHE_QUERY_BY_USER`` flag also reaches the legacy viz path. - """ - feature_flag_mock.is_feature_enabled.side_effect = ( - lambda feature=None: feature == "CACHE_QUERY_BY_USER" - ) - database = Database(database_name="d", sqlalchemy_uri="sqlite://") - obj = _viz_for(database) - - with override_user(User(username="alice")): - key_a = obj.cache_key(QUERY_OBJ) - with override_user(User(username="bob")): - key_b = obj.cache_key(QUERY_OBJ) - - assert key_a != key_b diff --git a/tests/unit_tests/test_viz_get_df_payload.py b/tests/unit_tests/test_viz_get_df_payload.py deleted file mode 100644 index 96ae6c5e0d8e..000000000000 --- a/tests/unit_tests/test_viz_get_df_payload.py +++ /dev/null @@ -1,244 +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. -from typing import Any -from unittest.mock import patch - -import pytest -from flask import current_app - -from superset import viz -from superset.common.db_query_status import QueryStatus -from superset.connectors.sqla.models import SqlaTable -from superset.errors import SupersetErrorType -from superset.exceptions import ( - OAuth2RedirectError, - QueryObjectValidationError, -) -from superset.models.core import Database - -QUERY_OBJ: dict[str, Any] = {"row_limit": 100, "from_dttm": None, "to_dttm": None} - - -def _viz() -> viz.BaseViz: - database = Database(database_name="d", sqlalchemy_uri="sqlite://") - datasource = SqlaTable( - table_name="t", - columns=[], - metrics=[], - main_dttm_col=None, - database=database, - ) - # ``force=True`` skips the data cache lookup branch so ``get_df`` is always - # invoked, which is what we want to assert error-handling against. - return viz.BaseViz( - datasource=datasource, - form_data={"viz_type": "table"}, - force=True, - ) - - -def _timeseries_viz(form_data: dict[str, Any]) -> viz.NVD3TimeSeriesViz: - database = Database(database_name="d", sqlalchemy_uri="sqlite://") - datasource = SqlaTable( - table_name="t", - columns=[], - metrics=[], - main_dttm_col=None, - database=database, - ) - base_form_data: dict[str, Any] = {"viz_type": "line", "metrics": ["value"]} - base_form_data.update(form_data) - return viz.NVD3TimeSeriesViz( - datasource=datasource, - form_data=base_form_data, - force=True, - ) - - -def _resample_df() -> Any: - import pandas as pd - - from superset.utils.core import DTTM_ALIAS - - return pd.DataFrame( - { - DTTM_ALIAS: pd.to_datetime(["2024-01-01", "2024-01-02", "2024-01-03"]), - "value": [1, 2, 3], - } - ) - - -def test_process_data_rejects_unknown_resample_method() -> None: - """ - A resample method outside the allowlist must raise - ``QueryObjectValidationError`` before any dynamic dispatch happens. - """ - obj = _timeseries_viz({"resample_rule": "1D", "resample_method": "__class__"}) - - with pytest.raises(QueryObjectValidationError): - obj.process_data(_resample_df()) - - -def test_process_data_accepts_allowlisted_resample_method() -> None: - """ - A valid resample method (e.g. ``mean``) is applied successfully. - """ - obj = _timeseries_viz({"resample_rule": "1D", "resample_method": "mean"}) - - result = obj.process_data(_resample_df()) - - assert result is not None - assert not result.empty - - -def test_run_extra_queries_rejects_excessive_time_compare() -> None: - """ - Requesting more time-shift comparisons than ``VIZ_TIME_COMPARE_MAX`` must - raise ``QueryObjectValidationError`` before any sub-queries are issued. - """ - max_compare = current_app.config["VIZ_TIME_COMPARE_MAX"] - obj = _timeseries_viz( - {"time_compare": [f"{i} days ago" for i in range(1, max_compare + 2)]} - ) - - with pytest.raises(QueryObjectValidationError): - obj.run_extra_queries() - - -def test_deck_multi_rejects_excessive_sub_slices() -> None: - """ - Requesting more deck.gl sub-slices than ``DECK_MULTI_MAX_SLICES`` must raise - ``QueryObjectValidationError`` before any sub-query is issued. - """ - database = Database(database_name="d", sqlalchemy_uri="sqlite://") - datasource = SqlaTable( - table_name="t", - columns=[], - metrics=[], - main_dttm_col=None, - database=database, - ) - max_slices = current_app.config["DECK_MULTI_MAX_SLICES"] - obj = viz.DeckGLMultiLayer( - datasource=datasource, - form_data={ - "viz_type": "deck_multi", - "deck_slices": list(range(max_slices + 1)), - }, - force=True, - ) - - with pytest.raises(QueryObjectValidationError): - obj.get_data(_resample_df()) - - -def test_get_df_payload_propagates_oauth2_redirect_error() -> None: - """ - OAuth2RedirectError (a SupersetErrorException) must propagate out of - ``get_df_payload`` so the global Flask error handler can serialize it. - """ - obj = _viz() - oauth_exc = OAuth2RedirectError( - url="https://accounts.example.com/o/oauth2/v2/auth?...", - tab_id="tab-123", - redirect_uri="https://superset.example.com/oauth2/redirect", - ) - - with patch.object(viz.BaseViz, "get_df", side_effect=oauth_exc): - with pytest.raises(OAuth2RedirectError) as exc_info: - obj.get_df_payload(QUERY_OBJ) - - assert exc_info.value.error.error_type == SupersetErrorType.OAUTH2_REDIRECT - assert exc_info.value.error.extra == { - "url": "https://accounts.example.com/o/oauth2/v2/auth?...", - "tab_id": "tab-123", - "redirect_uri": "https://superset.example.com/oauth2/redirect", - } - - -def test_get_df_payload_captures_generic_exception_as_viz_get_df_error() -> None: - """ - Non-Superset exception raised by ``get_df`` are downgraded to a - ``VIZ_GET_DF_ERROR`` entry on ``self.errors``. - """ - obj = _viz() - - with patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")): - payload = obj.get_df_payload(QUERY_OBJ) - - assert obj.status == QueryStatus.FAILED - assert payload["status"] == QueryStatus.FAILED - assert len(obj.errors) == 1 - assert obj.errors[0]["error_type"] == SupersetErrorType.VIZ_GET_DF_ERROR - assert obj.errors[0]["message"] == "boom" - - -def test_get_df_payload_captures_query_object_validation_error() -> None: - """ - ``QueryObjectValidationError`` is reported as ``VIZ_GET_DF_ERROR``. - """ - obj = _viz() - - with patch.object( - viz.BaseViz, - "get_df", - side_effect=QueryObjectValidationError("bad query"), - ): - payload = obj.get_df_payload(QUERY_OBJ) - - assert obj.status == QueryStatus.FAILED - assert payload["status"] == QueryStatus.FAILED - assert len(obj.errors) == 1 - assert obj.errors[0]["error_type"] == SupersetErrorType.VIZ_GET_DF_ERROR - assert obj.errors[0]["message"] == "bad query" - - -def test_get_df_payload_hides_stacktrace_when_show_stacktrace_disabled() -> None: - """ - The error payload must not expose a stacktrace when neither ``debug`` nor - ``SHOW_STACKTRACE`` is enabled. - """ - obj = _viz() - - # The test app runs with ``debug`` disabled, so toggling ``SHOW_STACKTRACE`` - # off is enough to exercise the hidden-stacktrace branch. - assert current_app.debug is False - with ( - patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")), - patch.dict(current_app.config, {"SHOW_STACKTRACE": False}), - ): - payload = obj.get_df_payload(QUERY_OBJ) - - assert obj.status == QueryStatus.FAILED - assert payload["stacktrace"] is None - - -def test_get_df_payload_shows_stacktrace_when_show_stacktrace_enabled() -> None: - """ - The error payload must expose a stacktrace when ``SHOW_STACKTRACE`` is enabled. - """ - obj = _viz() - - with ( - patch.object(viz.BaseViz, "get_df", side_effect=RuntimeError("boom")), - patch.dict(current_app.config, {"SHOW_STACKTRACE": True}), - ): - payload = obj.get_df_payload(QUERY_OBJ) - - assert obj.status == QueryStatus.FAILED - assert payload["stacktrace"] is not None - assert "RuntimeError" in payload["stacktrace"] From b71ea77080c2b189a6ce8bdd0323fac22d57a890 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 3 Jul 2026 10:21:44 -0700 Subject: [PATCH 25/84] docs: add UPDATING.md entries for the legacy viz pipeline removal and drop the working trackers Co-Authored-By: Claude Fable 5 --- SCOPE.md | 134 ---------------------------------------------------- UPDATES.md | 48 ------------------- UPDATING.md | 3 ++ 3 files changed, 3 insertions(+), 182 deletions(-) delete mode 100644 SCOPE.md delete mode 100644 UPDATES.md diff --git a/SCOPE.md b/SCOPE.md deleted file mode 100644 index 971f3bd9f0f8..000000000000 --- a/SCOPE.md +++ /dev/null @@ -1,134 +0,0 @@ - - -# SCOPE: Remove the legacy viz pipeline (`explore_json` + `viz.py`) - -> **This file is a working tracker for the `remove-legacy-viz-pipeline` feature branch.** -> It accumulates everything done on this branch and will be distilled into the final -> PR body. It is deleted before the feature branch merges to `master`. -> Companion file: [UPDATES.md](UPDATES.md) staging entries destined for `UPDATING.md`. - -## Goal - -Migrate every remaining `useLegacyApi: true` viz plugin to the v1 `/api/v1/chart/data` -endpoint (adding `buildQuery.ts` + `post_processing` operations / `transformProps` -reshaping as needed), then delete the deprecated `explore_json` endpoints and -`superset/viz.py` entirely, and drop the `legacy-` prefix from the surviving plugin -package names. Both endpoints have carried `@deprecated(eol_version="5.0.0")` gates; -this branch is the EOL. - -## Inventory: the 15 remaining legacy viz types - -| # | viz_type | Plugin | Strategy | Status | -|---|----------|--------|----------|--------| -| 1 | `para` | legacy-plugin-chart-parallel-coordinates | Convert in place (pass-through) | ✅ (merged) | -| 2 | `country_map` | legacy-plugin-chart-country-map | Convert in place (select/rename) | ✅ (merged) | -| 3 | `bullet` | legacy-preset-chart-nvd3/Bullet | Convert in place (single metric) | ✅ (merged) | -| 4 | `chord` | legacy-plugin-chart-chord | Convert in place (matrix in transformProps) | ✅ (merged) | -| 5 | `world_map` | legacy-plugin-chart-world-map | Convert in place (country join in JS) | ✅ (merged) | -| 6 | `paired_ttest` | legacy-plugin-chart-paired-t-test | Convert in place (pivot op) | ✅ (merged) | -| 7 | `time_table` | src/visualizations/TimeTable | Convert in place (pivot op) | ✅ (merged) | -| 8 | `cal_heatmap` | legacy-plugin-chart-calendar | Convert in place (pivot + epoch keys in JS) | ✅ (merged) | -| 9 | `horizon` | legacy-plugin-chart-horizon | Convert in place (timeseries pipeline) | ✅ (merged) | -| 10 | `rose` | legacy-plugin-chart-rose | Convert in place (timeseries pipeline) | ✅ (merged) | -| 11 | `time_pivot` | legacy-preset-chart-nvd3/TimePivot | Convert in place (pivot + rank in JS) | ✅ (merged) | -| 12 | `bubble` | legacy-preset-chart-nvd3/Bubble | Retarget → `bubble_v2` (MigrateViz exists, needs Alembic revision) | ✅ (merged) | -| 13 | `compare` | legacy-preset-chart-nvd3/Compare | Retarget → `echarts_timeseries_line` (new MigrateViz) | ✅ (merged) | -| 14 | `partition` | legacy-plugin-chart-partition | Convert in place (hierarchy in transformProps) | ✅ (merged) | -| 15 | `deck_multi` | preset-chart-deckgl/Multi | Client-side filter-metadata merge + stub buildQuery | ✅ (merged) | - -"Convert in place" = keep the renderer and the `viz_type` key, add a modern -`buildQuery.ts`, move the `viz.py` `get_data()` reshape into `transformProps.ts`, -flip `useLegacyApi` off. No saved-chart DB migration needed since `viz_type` is -unchanged. "Retarget" = migrate saved charts to an existing modern chart via a -`MigrateViz` processor + Alembic revision, then delete the legacy plugin. - -## Phases / sub-PRs (each targets this feature branch) - -### Phase 0 — Dead code deletion -- [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: -- [ ] `buildQuery.ts` returning a `buildQueryContext(...)` with needed `post_processing` operators -- [ ] `transformProps.ts` ports the `viz.py` `get_data()` reshape (where not expressible as post_processing) -- [ ] `useLegacyApi` removed from plugin metadata -- [ ] Jest tests: buildQuery output + transformProps against fixtures captured from the legacy pipeline -- [ ] Verify legacy saved-chart `form_data` (`granularity_sqla`/`time_range`) flows through `buildQueryContext` — add a normalizing same-viz-type `MigrateViz` if not -- [ ] For retargets: `MigrateViz` processor + `superset/cli/viz_migrations.py` registration + Alembic revision (with MySQL MEDIUMTEXT widening) + migration tests -- [ ] UPDATES.md entry if user-facing behavior changes - -### Phase 2 — Backend removal (#41750) -- [x] Remove `explore_json` / `explore_json_data` / `generate_json` from `superset/views/core.py` (incl. CSV/XLSX/async branches) -- [x] Remove CSRF exemption (`superset/config.py`), the `load_explore_json_into_cache` celery task and async manager plumbing -- [x] Delete `superset/viz.py`; unwind importers: `views/utils.py` (`get_viz` + explore-cache perm checkers), `models/slice.py` (`Slice.viz`, `explore_json_url`), `commands/chart/warm_up_cache.py`, `common/query_context_processor.py` (annotation path), `security/manager.py` (`BaseViz` overload + public-role `can_explore_json` grant) -- [x] Delete `tests/unit_tests/test_viz_*.py`, `tests/integration_tests/viz_tests.py`; retarget/remove other explore_json test coverage -- [x] Frontend plumbing: remove `useLegacyApi` from `ChartMetadata`, `shouldUseLegacyApi`, `/explore_json/` branches in `exploreUtils`, `chartAction.ts`, `ChartClient.ts`, `StatefulChart.tsx`, `DrillByModal.tsx` - -### Phase 3 — Renames (#41751) -- [x] `legacy-plugin-chart-*` → `plugin-chart-*` (dirs, npm names, `file:` deps, MainPreset imports, Storybook/docs) -- [x] `legacy-preset-chart-nvd3` → `preset-chart-nvd3` (bullet, time_pivot survive there) -- [x] "(legacy)" chart names and Legacy gallery tags removed with each chart migration - -## Log of work completed - -(append entries as sub-PRs merge: date, sub-PR #, summary) - -- 2026-07-02 — #41715 Phase 0 merged: removed orphaned `viz.py` classes - (`MapboxViz`, `MapLibreViz`, `EventFlowViz`) and nvd3 BoxPlot leftovers. -- 2026-07-02 — #41716 `para` merged: buildQuery (series→columns, secondary - metric alias, sort-metric append, explicit orderby ownership), 8 Jest tests. -- 2026-07-02 — #41717 `country_map` merged: buildQuery (entity+metric), - country_id/metric rename in transformProps with both-labels guard, 15 tests. -- 2026-07-02 — #41718 `bullet` merged: buildQuery (single ungrouped metric), - records→{measures} reshape in nvd3 transformProps Bullet branch, 5 tests. -- 2026-07-02 — #41719 `chord`, #41720 `world_map`, #41721 `paired_ttest`, - #41723 `time_table`, #41724 `cal_heatmap` merged. Notable bot-caught fixes - along the way: ChartProps camelization bug in world_map's country join, - numeric group ordering in paired_ttest, comma escaping + sub-second keys + - grouped single-metric guard in time_table. -- 2026-07-03 — #41725 `horizon`, #41726 `rose` (server-side post_processing - pipeline; bot caught the legacy 'absolute'→'difference' comparison-type - mismatch), #41727 `time_pivot` (pandas offset rollback ported to JS, incl. - quarters) merged. All 11 convert-in-place charts are migrated. -- 2026-07-03 — #41728 `bubble`, #41729 `partition`, #41730 `deck_multi`, - #41732 ordering parity, #41738 `compare` merged. **All 15 legacy charts - are migrated.** Notable: bubble's revision initially chained off the wrong - alembic head (filename timestamps lie — walk the down_revision graph); - the world bank example now ships `bubble_v2`. -- Open PRs: #41750 Phase 2 (explore_json + viz.py + frontend useLegacyApi - plumbing removal), #41751 Phase 3 (legacy- prefix renames: - plugin-chart-*, preset-chart-nvd3). - -- 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). -- **partition** → attempting convert-in-place; if the transformProps port proves unmaintainable, fall back to proposing deprecation on dev@. -- Feature branch lives on `apache/superset` so phase PRs can target it. Tracking files (SCOPE.md, UPDATES.md) are stripped before final merge. -- Tracking files are updated ONLY via direct commits to the feature branch (sub-PRs editing SCOPE.md all conflicted after the first squash-merge). - -## Open questions - -(record anything needing Evan's / community input) diff --git a/UPDATES.md b/UPDATES.md deleted file mode 100644 index 127ed1ab06e5..000000000000 --- a/UPDATES.md +++ /dev/null @@ -1,48 +0,0 @@ - - -# UPDATES: staged entries for UPDATING.md - -> **Working tracker for the `remove-legacy-viz-pipeline` feature branch.** -> Each bullet below is drafted in UPDATING.md's voice and will be moved into -> `UPDATING.md` under "Next" before the feature branch merges. This file is -> then deleted. - -## Breaking changes (drafts) - -- [ ] The deprecated `/superset/explore_json/` endpoints (deprecated since 3.0, - EOL 5.0.0) have been removed. All charts now use `/api/v1/chart/data`. -- [ ] `superset/viz.py` has been removed. Anything importing `superset.viz` - (custom code, plugins) must migrate to the QueryContext / - `pandas_postprocessing` pipeline. -- [ ] The `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` has been - removed. Third-party viz plugins that set it must provide a `buildQuery` - and use the v1 chart data API. -- [ ] Legacy plugin packages have been renamed to drop the `legacy-` prefix - (e.g. `@superset-ui/legacy-plugin-chart-chord` → `@superset-ui/plugin-chart-chord`). - (enumerate final list here) -- [ ] "Bubble Chart (legacy)" (`bubble`) charts are automatically migrated to the - ECharts Bubble Chart (`bubble_v2`). -- [ ] "Time-series Percent Change" (`compare`) charts are automatically migrated to - the ECharts Line Chart. The interactive re-basing of the nvd3 renderer is not - preserved. - -## Non-breaking notes (drafts) - -(add as they come up — e.g. behavior nuances found while porting get_data reshapes) diff --git a/UPDATING.md b/UPDATING.md index 1f2e73765e2d..cf80f076964c 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -193,6 +193,9 @@ are the intended model going forward; deprecating and removing implicit viewersh in a later major version. - [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored. +- [41714](https://github.com/apache/superset/pull/41714): **Breaking — the legacy `explore_json` chart-data pipeline is removed** at its long-declared `5.0.0` EOL. The `/superset/explore_json/` and `/superset/explore_json/data/` endpoints, `superset/viz.py`, the `Slice.viz` property, the `get_viz` factory, the `load_explore_json_into_cache` celery task and the `viz=` overload of `security_manager.raise_for_access` are gone. Anything importing `superset.viz` must migrate to the QueryContext / `pandas_postprocessing` pipeline behind `/api/v1/chart/data`. All 15 remaining legacy charts were migrated first: most keep their `viz_type` and renderer (no action needed for saved charts), while saved nvd3 Bubble charts are auto-migrated to the ECharts Bubble Chart (`bubble_v2`) and saved "Time-series Percent Change" (`compare`) charts to the ECharts Line Chart — the nvd3 renderer's interactive percent re-basing is not preserved. The deck.gl Multiple Layers chart now fetches its layers entirely client-side; its initial autozoom falls back to the saved viewport, and dashboard filter badges no longer aggregate child-layer filter metadata. + +- [41714](https://github.com/apache/superset/pull/41714): **Breaking for third-party viz plugins** — the `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` is removed. Plugins that set it must provide a `buildQuery` and consume `/api/v1/chart/data`. The migrated first-party packages also drop their `legacy-` prefix: `@superset-ui/legacy-plugin-chart-{calendar,chord,country-map,horizon,paired-t-test,parallel-coordinates,partition,rose,world-map}` → `@superset-ui/plugin-chart-*`, and `@superset-ui/legacy-preset-chart-nvd3` → `@superset-ui/preset-chart-nvd3`. The `can_explore_json` permission is no longer created or granted; custom roles referencing it should switch to the `can_read` permissions on `Chart`. - [41813](https://github.com/apache/superset/pull/41813): `redis` (the Python client, `redis-py`) is bumped from 5.3.1 to 8.0.1. redis-py 8 changes several connection defaults; Superset's own Redis-backed features (`GLOBAL_ASYNC_QUERIES_CACHE_BACKEND`, `DISTRIBUTED_COORDINATION_CONFIG`, and the MCP Redis store) explicitly pin the pre-upgrade behavior so this bump is a no-op for them: the wire protocol stays RESP2 (not the new RESP3 default, which requires Redis/Sentinel 6+ to speak `HELLO`) and there is still no socket timeout by default (redis-py 8 defaults to 5s, which could otherwise newly time out large cached payloads or slow networks). The no-timeout default can now be overridden via two new config keys, `CACHE_REDIS_SOCKET_TIMEOUT` / `CACHE_REDIS_SOCKET_CONNECT_TIMEOUT`, on any `CacheConfig` dict using `CACHE_TYPE: RedisCache` or `RedisSentinelCache`. Separately, redis-py 6+ changed the default for `ssl_check_hostname` from `False` to `True` for SSL connections using `ssl_cert_reqs="required"` (the default) — this is a security improvement, so it has **not** been reverted; deployments with `CACHE_REDIS_SSL=True` whose certificates lack a hostname matching the connection address should set `CACHE_REDIS_SSL_CERT_REQS="none"` (disables cert verification entirely, matching hostname-check bypass) or replace the certificate. General-purpose cache/results backends configured via `CACHE_CONFIG` / `DATA_CACHE_CONFIG` / `RESULTS_BACKEND` with `CACHE_TYPE: RedisCache` go through `flask-caching`'s own Redis backend (outside Superset's code) and are subject to the same new defaults; pass `socket_timeout` / `protocol` via `CACHE_OPTIONS` there if needed. Celery broker and result-backend connections (built by `kombu`, also outside Superset's code) keep their no-socket-timeout behavior (`kombu` passes `socket_timeout=None` explicitly) but do **not** pin the wire protocol, so they follow redis-py's RESP3 default — which requires a Redis server new enough to speak `HELLO` (Redis 6+). Deployments using a pre-6.0 Redis server (EOL) as a Celery broker should upgrade the server before taking this bump. From 83e7098ee8b778f316a9c70db48d9b35b8470d84 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 7 Jul 2026 10:05:07 -0700 Subject: [PATCH 26/84] fix: restore OAuth2RedirectError import after rebase Co-Authored-By: Claude Opus 4.8 --- tests/unit_tests/tasks/test_async_queries.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_tests/tasks/test_async_queries.py b/tests/unit_tests/tasks/test_async_queries.py index 2000627f060a..435251cbf002 100644 --- a/tests/unit_tests/tasks/test_async_queries.py +++ b/tests/unit_tests/tasks/test_async_queries.py @@ -23,6 +23,7 @@ from superset.commands.chart.exceptions import ChartDataQueryFailedError from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( + OAuth2RedirectError, SupersetErrorException, SupersetErrorsException, ) From 6a5049daed7a8aed2a343d95c1d19412b7797dfc Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 7 Jul 2026 19:04:38 -0700 Subject: [PATCH 27/84] fix: repair stale package-lock.json and alembic migration branch - Remove dangling package-lock.json link entries for the removed legacy-* plugin workspaces and regenerate entries for their renamed plugin-chart-* counterparts; npm ci was failing with EUSAGE because the lockfile referenced workspace paths that no longer exist on disk. - Re-parent the bubble-chart-to-echarts migration (d4e5f6a7b8c9) onto the current single migration head (3a8e6f2c1b95) instead of a now-superseded ancestor, resolving the "Multiple head revisions" alembic error that was failing test-sqlite/test-postgres/test-mysql/cypress/playwright. Co-Authored-By: Claude Opus 4.8 --- superset-frontend/package-lock.json | 327 +++++++++++++++--- ...6a7b8c9_migrate_bubble_chart_to_echarts.py | 4 +- 2 files changed, 285 insertions(+), 46 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 08ba786868f0..f82be3bd74f8 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -51,25 +51,25 @@ "@scarf/scarf": "^1.4.0", "@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls", "@superset-ui/core": "file:./packages/superset-ui-core", - "@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar", - "@superset-ui/legacy-plugin-chart-chord": "file:./plugins/legacy-plugin-chart-chord", - "@superset-ui/legacy-plugin-chart-country-map": "file:./plugins/legacy-plugin-chart-country-map", - "@superset-ui/legacy-plugin-chart-horizon": "file:./plugins/legacy-plugin-chart-horizon", - "@superset-ui/legacy-plugin-chart-paired-t-test": "file:./plugins/legacy-plugin-chart-paired-t-test", - "@superset-ui/legacy-plugin-chart-parallel-coordinates": "file:./plugins/legacy-plugin-chart-parallel-coordinates", - "@superset-ui/legacy-plugin-chart-partition": "file:./plugins/legacy-plugin-chart-partition", - "@superset-ui/legacy-plugin-chart-rose": "file:./plugins/legacy-plugin-chart-rose", - "@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map", - "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3", "@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table", + "@superset-ui/plugin-chart-calendar": "file:./plugins/plugin-chart-calendar", "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", + "@superset-ui/plugin-chart-chord": "file:./plugins/plugin-chart-chord", + "@superset-ui/plugin-chart-country-map": "file:./plugins/plugin-chart-country-map", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars", + "@superset-ui/plugin-chart-horizon": "file:./plugins/plugin-chart-horizon", + "@superset-ui/plugin-chart-paired-t-test": "file:./plugins/plugin-chart-paired-t-test", + "@superset-ui/plugin-chart-parallel-coordinates": "file:./plugins/plugin-chart-parallel-coordinates", + "@superset-ui/plugin-chart-partition": "file:./plugins/plugin-chart-partition", "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table", "@superset-ui/plugin-chart-point-cluster-map": "file:./plugins/plugin-chart-point-cluster-map", + "@superset-ui/plugin-chart-rose": "file:./plugins/plugin-chart-rose", "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table", "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", + "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", "@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl", + "@superset-ui/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", "@types/d3-selection": "^3.0.11", @@ -10770,60 +10770,48 @@ "resolved": "packages/generator-superset", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-calendar": { - "resolved": "plugins/legacy-plugin-chart-calendar", - "link": true - }, - "node_modules/@superset-ui/legacy-plugin-chart-chord": { - "resolved": "plugins/legacy-plugin-chart-chord", + "node_modules/@superset-ui/plugin-chart-ag-grid-table": { + "resolved": "plugins/plugin-chart-ag-grid-table", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-country-map": { - "resolved": "plugins/legacy-plugin-chart-country-map", + "node_modules/@superset-ui/plugin-chart-calendar": { + "resolved": "plugins/plugin-chart-calendar", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-horizon": { - "resolved": "plugins/legacy-plugin-chart-horizon", + "node_modules/@superset-ui/plugin-chart-cartodiagram": { + "resolved": "plugins/plugin-chart-cartodiagram", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-paired-t-test": { - "resolved": "plugins/legacy-plugin-chart-paired-t-test", + "node_modules/@superset-ui/plugin-chart-chord": { + "resolved": "plugins/plugin-chart-chord", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-parallel-coordinates": { - "resolved": "plugins/legacy-plugin-chart-parallel-coordinates", + "node_modules/@superset-ui/plugin-chart-country-map": { + "resolved": "plugins/plugin-chart-country-map", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-partition": { - "resolved": "plugins/legacy-plugin-chart-partition", + "node_modules/@superset-ui/plugin-chart-echarts": { + "resolved": "plugins/plugin-chart-echarts", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-rose": { - "resolved": "plugins/legacy-plugin-chart-rose", + "node_modules/@superset-ui/plugin-chart-handlebars": { + "resolved": "plugins/plugin-chart-handlebars", "link": true }, - "node_modules/@superset-ui/legacy-plugin-chart-world-map": { - "resolved": "plugins/legacy-plugin-chart-world-map", + "node_modules/@superset-ui/plugin-chart-horizon": { + "resolved": "plugins/plugin-chart-horizon", "link": true }, - "node_modules/@superset-ui/legacy-preset-chart-nvd3": { - "resolved": "plugins/legacy-preset-chart-nvd3", + "node_modules/@superset-ui/plugin-chart-paired-t-test": { + "resolved": "plugins/plugin-chart-paired-t-test", "link": true }, - "node_modules/@superset-ui/plugin-chart-ag-grid-table": { - "resolved": "plugins/plugin-chart-ag-grid-table", + "node_modules/@superset-ui/plugin-chart-parallel-coordinates": { + "resolved": "plugins/plugin-chart-parallel-coordinates", "link": true }, - "node_modules/@superset-ui/plugin-chart-cartodiagram": { - "resolved": "plugins/plugin-chart-cartodiagram", - "link": true - }, - "node_modules/@superset-ui/plugin-chart-echarts": { - "resolved": "plugins/plugin-chart-echarts", - "link": true - }, - "node_modules/@superset-ui/plugin-chart-handlebars": { - "resolved": "plugins/plugin-chart-handlebars", + "node_modules/@superset-ui/plugin-chart-partition": { + "resolved": "plugins/plugin-chart-partition", "link": true }, "node_modules/@superset-ui/plugin-chart-pivot-table": { @@ -10834,6 +10822,10 @@ "resolved": "plugins/plugin-chart-point-cluster-map", "link": true }, + "node_modules/@superset-ui/plugin-chart-rose": { + "resolved": "plugins/plugin-chart-rose", + "link": true + }, "node_modules/@superset-ui/plugin-chart-table": { "resolved": "plugins/plugin-chart-table", "link": true @@ -10842,10 +10834,18 @@ "resolved": "plugins/plugin-chart-word-cloud", "link": true }, + "node_modules/@superset-ui/plugin-chart-world-map": { + "resolved": "plugins/plugin-chart-world-map", + "link": true + }, "node_modules/@superset-ui/preset-chart-deckgl": { "resolved": "plugins/preset-chart-deckgl", "link": true }, + "node_modules/@superset-ui/preset-chart-nvd3": { + "resolved": "plugins/preset-chart-nvd3", + "link": true + }, "node_modules/@superset-ui/switchboard": { "resolved": "packages/superset-ui-switchboard", "link": true @@ -44226,6 +44226,45 @@ "node": ">=12" } }, + "plugins/plugin-chart-calendar": { + "name": "@superset-ui/plugin-chart-calendar", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3-array": "^3.2.4", + "d3-selection": "^3.0.0", + "d3-tip": "^0.9.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@emotion/react": "^11.4.1", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-calendar/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "plugins/plugin-chart-calendar/node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "plugins/plugin-chart-cartodiagram": { "name": "@superset-ui/plugin-chart-cartodiagram", "version": "0.0.1", @@ -44254,6 +44293,58 @@ "react-dom": "^18.3.0" } }, + "plugins/plugin-chart-chord": { + "name": "@superset-ui/plugin-chart-chord", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "prop-types": "^15.8.1", + "react": "^19.2.7" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*" + } + }, + "plugins/plugin-chart-chord/node_modules/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "plugins/plugin-chart-country-map": { + "name": "@superset-ui/plugin-chart-country-map", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "d3-array": "^3.2.4", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-country-map/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "plugins/plugin-chart-echarts": { "name": "@superset-ui/plugin-chart-echarts", "version": "0.20.3", @@ -44341,6 +44432,84 @@ "sprintf-js": ">= 1.1.1 < 2" } }, + "plugins/plugin-chart-horizon": { + "name": "@superset-ui/plugin-chart-horizon", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3-array": "^3.2.4", + "d3-scale": "^4.0.2", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-horizon/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "plugins/plugin-chart-paired-t-test": { + "name": "@superset-ui/plugin-chart-paired-t-test", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "distributions": "^2.2.0", + "prop-types": "^15.8.1", + "reactable": "^1.1.0" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-parallel-coordinates": { + "name": "@superset-ui/plugin-chart-parallel-coordinates", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3v3": "npm:d3@3.5.17", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-partition": { + "name": "@superset-ui/plugin-chart-partition", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "d3-hierarchy": "^3.1.2", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "@testing-library/jest-dom": "*", + "@testing-library/react": "^14.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + } + }, "plugins/plugin-chart-pivot-table": { "name": "@superset-ui/plugin-chart-pivot-table", "version": "0.20.3", @@ -44384,6 +44553,23 @@ "react-dom": "^18.3.0" } }, + "plugins/plugin-chart-rose": { + "name": "@superset-ui/plugin-chart-rose", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "nvd3-fork": "^2.0.5", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@emotion/react": "^11.4.1", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, "plugins/plugin-chart-table": { "name": "@superset-ui/plugin-chart-table", "version": "0.20.3", @@ -44449,6 +44635,36 @@ "react": "^18.3.0" } }, + "plugins/plugin-chart-world-map": { + "name": "@superset-ui/plugin-chart-world-map", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "d3-array": "^3.2.4", + "datamaps": "^0.5.10", + "prop-types": "^15.8.1", + "tinycolor2": "^1.6.0" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "react": "^18.3.0" + } + }, + "plugins/plugin-chart-world-map/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "plugins/preset-chart-deckgl": { "name": "@superset-ui/preset-chart-deckgl", "version": "1.0.0", @@ -44530,6 +44746,29 @@ "engines": { "node": ">=12" } + }, + "plugins/preset-chart-nvd3": { + "name": "@superset-ui/preset-chart-nvd3", + "version": "0.20.3", + "license": "Apache-2.0", + "dependencies": { + "d3": "^3.5.17", + "d3-tip": "^0.9.1", + "dompurify": "^3.4.11", + "fast-safe-stringify": "^2.1.1", + "lodash": "^4.18.1", + "lodash-es": "^4.18.1", + "nvd3-fork": "^2.0.5", + "prop-types": "^15.8.1", + "urijs": "^1.19.11" + }, + "peerDependencies": { + "@apache-superset/core": "*", + "@superset-ui/chart-controls": "*", + "@superset-ui/core": "*", + "dayjs": "^1.11.21", + "react": "^18.3.0" + } } } } diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py index d80c8b50dd92..cd21d0cd2ad4 100644 --- a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -17,7 +17,7 @@ """migrate_bubble_chart_to_echarts Revision ID: d4e5f6a7b8c9 -Revises: b4a3f2e1d0c9 +Revises: 3a8e6f2c1b95 Create Date: 2026-07-02 21:00:00.000000 """ @@ -31,7 +31,7 @@ # revision identifiers, used by Alembic. revision = "d4e5f6a7b8c9" -down_revision = "b4a3f2e1d0c9" +down_revision = "3a8e6f2c1b95" def upgrade(): From ba795ccfa0705f0cd1d4eae0a2d296228073b408 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 8 Jul 2026 04:38:50 -0700 Subject: [PATCH 28/84] fix: format Multi.tsx and update Multi.color.test.tsx for registry-based layer loading Multi.tsx now resolves sublayer buildQuery/transformProps via the chart plugin registries instead of the removed legacy explore.ts helper; update the color test's mocks to register deck_scatter/deck_arc stubs and mock SupersetClient.post (matching the new fetch call) instead of .get. Co-Authored-By: Claude Opus 4.8 --- .../src/Multi/Multi.color.test.tsx | 54 ++++++++++++++----- .../preset-chart-deckgl/src/Multi/Multi.tsx | 3 +- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.color.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.color.test.tsx index 52daea451562..c7404b5d0d37 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.color.test.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.color.test.tsx @@ -21,7 +21,7 @@ import '@testing-library/jest-dom'; import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; -import { DatasourceType, SupersetClient } from '@superset-ui/core'; +import { DatasourceType, JsonObject, SupersetClient } from '@superset-ui/core'; import DeckMulti from './Multi'; // Capture the layers handed to the DeckGL container so we can inspect the @@ -48,10 +48,38 @@ jest.mock('../DeckGLContainer', () => ({ jest.mock('@superset-ui/core', () => ({ ...jest.requireActual('@superset-ui/core'), SupersetClient: { - get: jest.fn(), + post: jest.fn(), }, })); +// register stub buildQuery/transformProps for the layer types the tests use. +// The stub transformProps simply echoes the fetched records through as +// layer features, so `addColorToFeatures` (exercised in Multi.tsx) resolves +// colors from the same raw records the tests assert against. +const { getChartBuildQueryRegistry, getChartTransformPropsRegistry } = + jest.requireActual('@superset-ui/core'); +['deck_scatter', 'deck_arc'].forEach(vizType => { + getChartBuildQueryRegistry().registerValue( + vizType, + (formData: Record) => ({ + datasource: 'test_datasource', + queries: [{}], + form_data: formData, + }), + ); + getChartTransformPropsRegistry().registerValue( + vizType, + (chartProps: { queriesData: { data: JsonObject[] }[] }) => ({ + payload: { + data: { + features: chartProps.queriesData?.[0]?.data || [], + mapboxApiKey: 'test-key', + }, + }, + }), + ); +}); + const mockStore = configureStore({ reducer: { dataMask: () => ({}), @@ -119,14 +147,16 @@ beforeEach(() => { jest.clearAllMocks(); mockLayerCapture.layers = []; // The scatter sublayer query returns features tagged with a category column. - (SupersetClient.get as jest.Mock).mockResolvedValue({ + (SupersetClient.post as jest.Mock).mockResolvedValue({ json: { - data: { - features: [ - { position: [0, 0], radius: 1, cat_color: 'A' }, - { position: [1, 1], radius: 1, cat_color: 'B' }, - ], - }, + result: [ + { + data: [ + { position: [0, 0], radius: 1, cat_color: 'A' }, + { position: [1, 1], radius: 1, cat_color: 'B' }, + ], + }, + ], }, }); }); @@ -221,11 +251,9 @@ test('keeps fixed source and target colors for arc subslices saved before the co }, }, }; - (SupersetClient.get as jest.Mock).mockResolvedValue({ + (SupersetClient.post as jest.Mock).mockResolvedValue({ json: { - data: { - features: [{ sourcePosition: [0, 0], targetPosition: [1, 1] }], - }, + result: [{ data: [{ sourcePosition: [0, 0], targetPosition: [1, 1] }] }], }, }); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index 50387dd0744b..a0db73367b73 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -476,8 +476,7 @@ const DeckMulti = (props: DeckMultiProps) => { ), ).then(slices => slices.filter( - (slice): slice is { slice_id: number } & JsonObject => - slice !== null, + (slice): slice is { slice_id: number } & JsonObject => slice !== null, ), ), [], From d59120a6324c97017104e0d59682814ae8b2e302 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 9 Jul 2026 07:29:10 -0700 Subject: [PATCH 29/84] fix: resolve alembic multiple-heads conflict in migration chain down_revision on d4e5f6a7b8c9 still pointed at 3a8e6f2c1b95, which master's add_subjects_tables migration (b1c2d3e4f5a6) also forked from, leaving two alembic heads and breaking `superset db upgrade` in every DB-backed CI job. Co-Authored-By: Claude Opus 4.8 --- ...7-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py index cd21d0cd2ad4..c3ada48b63b8 100644 --- a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -17,7 +17,7 @@ """migrate_bubble_chart_to_echarts Revision ID: d4e5f6a7b8c9 -Revises: 3a8e6f2c1b95 +Revises: b1c2d3e4f5a6 Create Date: 2026-07-02 21:00:00.000000 """ @@ -31,7 +31,7 @@ # revision identifiers, used by Alembic. revision = "d4e5f6a7b8c9" -down_revision = "3a8e6f2c1b95" +down_revision = "b1c2d3e4f5a6" def upgrade(): From dcda71e869327999c6ceee7978eadfbbc28fb216 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 10 Jul 2026 14:30:56 -0700 Subject: [PATCH 30/84] fix: point bubble-chart migration at master's actual alembic head down_revision on d4e5f6a7b8c9 pointed at b1c2d3e4f5a6 (add_subjects_tables), which already has a child on master (56cd24c07170 -> 8f3a1b2c4d5e). That left two alembic heads and broke `superset db upgrade` in every DB-backed CI job. Chain the migration after 8f3a1b2c4d5e, master's real current head, instead. Co-Authored-By: Claude Opus 4.8 --- ...7-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py index c3ada48b63b8..742b4df2a1a2 100644 --- a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -17,7 +17,7 @@ """migrate_bubble_chart_to_echarts Revision ID: d4e5f6a7b8c9 -Revises: b1c2d3e4f5a6 +Revises: 8f3a1b2c4d5e Create Date: 2026-07-02 21:00:00.000000 """ @@ -31,7 +31,7 @@ # revision identifiers, used by Alembic. revision = "d4e5f6a7b8c9" -down_revision = "b1c2d3e4f5a6" +down_revision = "8f3a1b2c4d5e" def upgrade(): From 71443460d1a8c2b056b274842f183a667e984789 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 14 Jul 2026 08:48:21 -0700 Subject: [PATCH 31/84] fix: resolve oxlint unused-var/param errors and remove orphaned legacy test Drops the dead `method` param from getChartDataRequest (the legacy GET/POST branching it supported no longer exists now that chart data always POSTs to /api/v1/chart/data), removes bubble-only NVD3Vis props (entity/maxBubbleSize/xField/yField/sizeField) and the unused module-level `formatter` left over from the bubble->bubble_v2 migration, and deletes plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts, an orphaned test for a src/ directory that no longer exists after the rename to preset-chart-nvd3 (superseded by plugins/preset-chart-nvd3/test/TimePivot.test.ts). Co-Authored-By: Claude Opus 4.8 --- .../test/TimePivot/controlPanel.test.ts | 57 ------------------- .../plugins/preset-chart-nvd3/src/NVD3Vis.ts | 6 -- .../src/components/Chart/chartAction.ts | 3 - 3 files changed, 66 deletions(-) delete mode 100644 superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts deleted file mode 100644 index 1830601713a1..000000000000 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/TimePivot/controlPanel.test.ts +++ /dev/null @@ -1,57 +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 { SqlaFormData } from '@superset-ui/core'; -import * as ChartControls from '@superset-ui/chart-controls'; -import controlPanel from '../../src/TimePivot/controlPanel'; - -const { __mockShiftMetric } = ChartControls as typeof ChartControls & { - __mockShiftMetric: jest.Mock; -}; - -jest.mock('@superset-ui/core', () => ({ - ...jest.requireActual('@superset-ui/core'), - t: (str: string) => str, -})); - -jest.mock('@superset-ui/chart-controls', () => { - const original = jest.requireActual('@superset-ui/chart-controls'); - const mockShiftMetric = jest.fn(() => 'shiftedMetric'); - return { - ...original, - getStandardizedControls: () => ({ - shiftMetric: mockShiftMetric, - }), - __mockShiftMetric: mockShiftMetric, - }; -}); - -describe('TimePivot Control Panel Config', () => { - test('should override formData metric using getStandardizedControls', () => { - const dummyFormData = { someProp: 'test' } as unknown as SqlaFormData; - const newFormData = controlPanel.formDataOverrides!(dummyFormData); - - // The original properties are spread correctly. - expect(newFormData.someProp).toBe('test'); - // The metric property should be replaced by the output of shiftMetric. - expect(newFormData.metric).toBe('shiftedMetric'); - - // Ensure that the mockShiftMetric function was called. - expect(__mockShiftMetric).toHaveBeenCalled(); - }); -}); diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts index 319c43fce510..ea62d7d27327 100644 --- a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts +++ b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts @@ -229,7 +229,6 @@ const propTypes = { }; const NOOP = () => {}; -const formatter = getNumberFormatter(); function nvd3Vis(element, props) { const { @@ -243,7 +242,6 @@ function nvd3Vis(element, props) { colorScheme, comparisonType, contribution, - entity, isBarStacked, isDonut, isPieLabelOutside, @@ -253,7 +251,6 @@ function nvd3Vis(element, props) { markerLines, markerLineLabels, markers, - maxBubbleSize, onBrushEnd = NOOP, onError = NOOP, pieLabelType, @@ -264,13 +261,11 @@ function nvd3Vis(element, props) { showLabels, showLegend, showMarkers, - sizeField, vizType, xAxisFormat, numberFormat, xAxisLabel, xAxisShowMinMax = false, - xField, xIsLogScale, xTicksLayout, yAxisFormat, @@ -278,7 +273,6 @@ function nvd3Vis(element, props) { yAxisLabel, yAxisShowMinMax = false, yAxis2ShowMinMax = false, - yField, yIsLogScale, sliceId, } = props; diff --git a/superset-frontend/src/components/Chart/chartAction.ts b/superset-frontend/src/components/Chart/chartAction.ts index c80cb7aad314..b0eff8f76152 100644 --- a/superset-frontend/src/components/Chart/chartAction.ts +++ b/superset-frontend/src/components/Chart/chartAction.ts @@ -274,7 +274,6 @@ export interface GetChartDataRequestParams { resultFormat?: string; resultType?: string; force?: boolean; - method?: 'GET' | 'POST'; requestParams?: RequestParams; ownState?: JsonObject; } @@ -461,7 +460,6 @@ export async function getChartDataRequest({ resultFormat = 'json', resultType = 'full', force = false, - method = 'POST' as const, requestParams = {}, ownState = {}, }: GetChartDataRequestParams): Promise { @@ -713,7 +711,6 @@ export function exploreJSON( resultFormat: 'json', resultType: 'full', force, - method: 'POST', requestParams, ownState, }); From 377e8e6345026e4bdc41148dd088321365a703db Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 15 Jul 2026 11:28:13 -0700 Subject: [PATCH 32/84] fix: rebase bubble-chart migration onto master's current alembic head master added strip_metricsqlexpressions_from_ag_grid_params (d24e6b0a9c7f) on top of shadow_live_row_indexes (8f3a1b2c4d5e) after this branch's bubble-chart migration was last pointed at 8f3a1b2c4d5e, producing two alembic heads and failing `db upgrade` across every integration/E2E job. Co-Authored-By: Claude Opus 4.8 --- ...7-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py index 742b4df2a1a2..ce7d202065ba 100644 --- a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -17,7 +17,7 @@ """migrate_bubble_chart_to_echarts Revision ID: d4e5f6a7b8c9 -Revises: 8f3a1b2c4d5e +Revises: d24e6b0a9c7f Create Date: 2026-07-02 21:00:00.000000 """ @@ -31,7 +31,7 @@ # revision identifiers, used by Alembic. revision = "d4e5f6a7b8c9" -down_revision = "8f3a1b2c4d5e" +down_revision = "d24e6b0a9c7f" def upgrade(): From a97329089082ec20b49a0488ebc14bc313bd53bb Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 12:11:13 -0700 Subject: [PATCH 33/84] fix(paired-t-test): restore the datetime column control The paired t-test chart's buildQuery sends is_timeseries, so the backend requires a datetime column, but its migrated control panel has no time section. Creating the chart fails with "Datetime column not provided as part table configuration and is required by this type of chart" with no way to supply one. Add the shared legacyTimeseriesTime section, matching its sibling timeseries charts (rose, horizon), so a granularity column can be selected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/controlPanel.ts | 6 ++++- .../test/controlPanel.test.ts | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 superset-frontend/plugins/plugin-chart-paired-t-test/test/controlPanel.test.ts diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts index 973cc0127a4e..205c0ea6d298 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/controlPanel.ts @@ -18,10 +18,14 @@ */ import { t } from '@apache-superset/core/translation'; import { validateNonEmpty } from '@superset-ui/core'; -import { ControlPanelConfig } from '@superset-ui/chart-controls'; +import { ControlPanelConfig, sections } from '@superset-ui/chart-controls'; const config: ControlPanelConfig = { controlPanelSections: [ + // The paired t-test is a timeseries query (buildQuery sets + // is_timeseries), so it needs a datetime column. Without this section the + // backend rejects the query with "Datetime column not provided...". + sections.legacyTimeseriesTime, { label: t('Query'), expanded: true, diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/test/controlPanel.test.ts new file mode 100644 index 000000000000..695b43bcb9f5 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/controlPanel.test.ts @@ -0,0 +1,27 @@ +/** + * 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 controlPanel from '../src/controlPanel'; + +test('exposes a datetime column control because the query is is_timeseries', () => { + // buildQuery sends is_timeseries, so the backend requires a datetime column. + // Without a granularity control the chart fails with "Datetime column not + // provided as part table configuration and is required by this type of + // chart". + expect(JSON.stringify(controlPanel)).toContain('granularity_sqla'); +}); From acc93dfe33391727cb41d3aea6db574f00146d43 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 12:38:00 -0700 Subject: [PATCH 34/84] fix(deck-multi): render the map instead of the no-results empty state deck.gl Multiple Layers issues no query of its own -- each layer is a saved chart that fetches its data client-side, so buildQuery is intentionally empty. Dropping useLegacyApi exposed the default enableNoResults=true, so the empty query response now shows "No results were returned for this query" and the map never renders. Set enableNoResults: false, matching the other self-fetching charts (Select filter, DeckglLayerVisibility). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/Multi/index.test.ts | 27 +++++++++++++++++++ .../preset-chart-deckgl/src/Multi/index.ts | 5 ++++ 2 files changed, 32 insertions(+) create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.test.ts diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.test.ts new file mode 100644 index 000000000000..5d42307ef520 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.test.ts @@ -0,0 +1,27 @@ +/** + * 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 MultiChartPlugin from './index'; + +test('suppresses the no-results empty state since it self-fetches its layers', () => { + // The multi-layer chart issues no query of its own, so its empty query + // response must not trigger "No results were returned for this query" (which + // would hide the map). Regression guard for the drop of useLegacyApi. + const plugin = new MultiChartPlugin(); + expect(plugin.metadata.enableNoResults).toBe(false); +}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts index 133b26f9f858..df4e2fa04a7a 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/index.ts @@ -29,6 +29,11 @@ const metadata = new ChartMetadata({ category: t('Map'), credits: ['https://uber.github.io/deck.gl'], description: t('Compose multiple layers together to form complex visuals.'), + // This chart issues no query of its own -- each layer is a saved chart that + // fetches its own data client-side. Without this, the empty query response + // renders the "No results were returned for this query" state instead of the + // map. useLegacyApi previously masked this. + enableNoResults: false, exampleGallery: [{ url: example, urlDark: exampleDark }], name: t('deck.gl Multiple Layers'), thumbnail, From 936ec951d5499d328f3ae8b6b6ca6a934d963745 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 12:59:31 -0700 Subject: [PATCH 35/84] fix(paired-t-test): compute p-values without the Node-only distributions dep The chart imported 'distributions' to compute the Student's t p-value, but that package references Node's Buffer global, which is only polyfilled in dev builds -- so the chart rendered in dev and threw "ReferenceError: Buffer is not defined" in production. Replace it with a self-contained, browser-safe two-sided p-value via the regularized incomplete beta function, verified against standard t-table critical values, and drop the dependency. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugin-chart-paired-t-test/package.json | 1 - .../src/TTestTable.tsx | 7 +- .../src/statistics.ts | 109 ++++++++++++++++++ .../test/statistics.test.ts | 34 ++++++ 4 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-paired-t-test/src/statistics.ts create mode 100644 superset-frontend/plugins/plugin-chart-paired-t-test/test/statistics.test.ts diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json index 675aa8417c2e..10de58072d1a 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json @@ -23,7 +23,6 @@ "lib" ], "dependencies": { - "distributions": "^2.2.0", "prop-types": "^15.8.1", "reactable": "^1.1.0" }, diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx index ca0881558e2e..7cbf834f5c61 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx @@ -17,8 +17,8 @@ * under the License. */ /* eslint-disable react/no-array-index-key, react/jsx-no-bind */ -import dist from 'distributions'; import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { studentTwoSidedPValue } from './statistics'; import { Table, Tr, Td, Thead, Th } from 'reactable'; interface DataPointValue { @@ -100,9 +100,8 @@ function TTestTable({ ), ); try { - return (2 * new dist.Studentt(finiteCount - 1).cdf(tvalue)).toFixed( - pValPrec, - ); // two-sided test + // two-sided test + return studentTwoSidedPValue(tvalue, finiteCount - 1).toFixed(pValPrec); } catch (error) { return NaN; } diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/statistics.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/src/statistics.ts new file mode 100644 index 000000000000..6e446d9d7a83 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/statistics.ts @@ -0,0 +1,109 @@ +/** + * 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. + */ + +/** + * Browser-safe Student's t statistics. Replaces the old `distributions` + * dependency, which referenced the Node-only `Buffer` global and so broke the + * chart in production builds (it only rendered in dev, where webpack polyfilled + * Buffer). + */ + +// Lanczos approximation of ln(gamma(z)). +function logGamma(z: number): number { + const c = [ + 0.99999999999980993, 676.5203681218851, -1259.1392167224028, + 771.32342877765313, -176.61502916214059, 12.507343278686905, + -0.13857109526572012, 9.9843695780195716e-6, 1.5056327351493116e-7, + ]; + const g = 7; + if (z < 0.5) { + return Math.log(Math.PI / Math.sin(Math.PI * z)) - logGamma(1 - z); + } + const zz = z - 1; + let x = c[0]; + for (let i = 1; i < g + 2; i += 1) { + x += c[i] / (zz + i); + } + const t = zz + g + 0.5; + return ( + 0.5 * Math.log(2 * Math.PI) + (zz + 0.5) * Math.log(t) - t + Math.log(x) + ); +} + +// Continued-fraction expansion for the incomplete beta function (Numerical +// Recipes `betacf`), evaluated with a modified Lentz's method. +function betacf(x: number, a: number, b: number): number { + const fpmin = 1e-30; + const qab = a + b; + const qap = a + 1; + const qam = a - 1; + let c = 1; + let d = 1 - (qab * x) / qap; + if (Math.abs(d) < fpmin) d = fpmin; + d = 1 / d; + let h = d; + for (let m = 1; m <= 200; m += 1) { + const m2 = 2 * m; + let aa = (m * (b - m) * x) / ((qam + m2) * (a + m2)); + d = 1 + aa * d; + if (Math.abs(d) < fpmin) d = fpmin; + c = 1 + aa / c; + if (Math.abs(c) < fpmin) c = fpmin; + d = 1 / d; + h *= d * c; + aa = (-(a + m) * (qab + m) * x) / ((a + m2) * (qap + m2)); + d = 1 + aa * d; + if (Math.abs(d) < fpmin) d = fpmin; + c = 1 + aa / c; + if (Math.abs(c) < fpmin) c = fpmin; + d = 1 / d; + const del = d * c; + h *= del; + if (Math.abs(del - 1) < 1e-12) break; + } + return h; +} + +// Regularized incomplete beta function I_x(a, b). +function incompleteBeta(x: number, a: number, b: number): number { + if (x <= 0) return 0; + if (x >= 1) return 1; + const front = Math.exp( + logGamma(a + b) - + logGamma(a) - + logGamma(b) + + a * Math.log(x) + + b * Math.log(1 - x), + ); + return x < (a + 1) / (a + b + 2) + ? (front * betacf(x, a, b)) / a + : 1 - (front * betacf(1 - x, b, a)) / b; +} + +/** + * Two-sided p-value for a t statistic with `df` degrees of freedom. + * + * Equivalent to the previous `2 * new dist.Studentt(df).cdf(-|t|)`: for the t + * distribution, P(|T| > |t|) = I_x(df/2, 1/2) with x = df / (df + t^2). + */ +export function studentTwoSidedPValue(t: number, df: number): number { + if (!Number.isFinite(t) || df <= 0) return NaN; + const x = df / (df + t * t); + return incompleteBeta(x, df / 2, 0.5); +} diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/statistics.test.ts b/superset-frontend/plugins/plugin-chart-paired-t-test/test/statistics.test.ts new file mode 100644 index 000000000000..9370bcb512b5 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/statistics.test.ts @@ -0,0 +1,34 @@ +/** + * 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 { studentTwoSidedPValue } from '../src/statistics'; + +test('matches standard two-sided t-table critical values', () => { + // t critical values from a standard table map to these two-sided p-values. + expect(studentTwoSidedPValue(-2.228, 10)).toBeCloseTo(0.05, 2); + expect(studentTwoSidedPValue(2.228, 10)).toBeCloseTo(0.05, 2); // sign agnostic + expect(studentTwoSidedPValue(-1.812, 10)).toBeCloseTo(0.1, 2); + expect(studentTwoSidedPValue(-2.086, 20)).toBeCloseTo(0.05, 2); + expect(studentTwoSidedPValue(-2.576, 1000)).toBeCloseTo(0.01, 2); +}); + +test('t = 0 gives p = 1, invalid inputs give NaN', () => { + expect(studentTwoSidedPValue(0, 10)).toBeCloseTo(1, 6); + expect(Number.isNaN(studentTwoSidedPValue(2, 0))).toBe(true); + expect(Number.isNaN(studentTwoSidedPValue(Infinity, 10))).toBe(true); +}); From dfc745c922d3dbd21fe50f9366d4d525bfbd6ce9 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 13:19:06 -0700 Subject: [PATCH 36/84] fix(nvd3,controls): time-pivot x-axis number format + single-line async multiselect - Time Pivot's x-axis is a numeric offset within the period (its x_axis_format defaults to SMART_NUMBER), but NVD3Vis ran it through getTimeFormatter, which made d3 print the literal string "SMART_NUMBER" on every tick. Format it as a number instead. (Pre-existing bug, bycatch.) - SelectAsyncControl never forwarded the Select 'oneLine' prop, so many selected tags wrapped outside the control's fixed-height box (visible on deck.gl Multiple Layers). Pass oneLine for multi mode so tags collapse to a '+N' tag. Co-Authored-By: Claude Opus 4.8 (1M context) --- superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts | 6 +++++- .../components/controls/SelectAsyncControl/index.tsx | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts index ea62d7d27327..ca750f1c96d4 100644 --- a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts +++ b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts @@ -463,7 +463,11 @@ function nvd3Vis(element, props) { } let xAxisFormatter; - if (isTimeSeries) { + // Time Pivot's x-axis is the elapsed offset within the period, not a + // timestamp, so it defaults x_axis_format to a number format (SMART_NUMBER). + // Running that through getTimeFormatter makes d3 treat "SMART_NUMBER" as a + // literal template and print it for every tick, so format it as a number. + if (isTimeSeries && vizType !== VizType.TimePivot) { xAxisFormatter = getTimeFormatter(xAxisFormat); // In tooltips, always use the verbose time format chart.interactiveLayer.tooltip.headerFormatter(smartDateVerboseFormatter); diff --git a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx index a21cdbabd50b..be7bf73db0ef 100644 --- a/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/SelectAsyncControl/index.tsx @@ -137,6 +137,9 @@ const SelectAsyncControl = ({ value={getValue()} header={} mode={multi ? 'multiple' : 'single'} + // Keep selected tags on a single line with a "+N" overflow instead of + // wrapping them outside the control's fixed-height box. + oneLine={multi} onChange={handleOnChange} options={options} placeholder={placeholder} From a5aa6196689fb658ffae1e80dd88f5c4e7f0f510 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 13:31:56 -0700 Subject: [PATCH 37/84] test(paired-t-test): mock the statistics module instead of removed distributions Removing the distributions dependency left TTestTable.test.tsx mocking a module the component no longer imports, so the p-value/significance tests failed. Mock ./statistics.studentTwoSidedPValue to a deterministic 0.02 instead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/TTestTable.test.tsx | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx index b8e6dd725e1d..fbaa8126d498 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx @@ -21,19 +21,12 @@ import '@testing-library/jest-dom'; import TTestTable from '../src/TTestTable'; import type { DataEntry } from '../src/TTestTable'; -// Mock the distributions module to return a predictable cdf value. -// cdf returns 0.01 so that p-value = 2 * 0.01 = 0.02 -jest.mock('distributions', () => { - class MockStudentt { - cdf(_x: number): number { - return 0.01; - } - } - return { - __esModule: true, - default: { Studentt: MockStudentt }, - }; -}); +// Mock the p-value computation to a predictable value (0.02) so the table's +// formatting and significance thresholds are deterministic. +jest.mock('../src/statistics', () => ({ + __esModule: true, + studentTwoSidedPValue: () => 0.02, +})); const mockData: DataEntry[] = [ { @@ -136,9 +129,8 @@ test('computes lift values correctly for non-control rows', async () => { }); }); -test('computes p-value using the mocked distributions module', async () => { - // Mock cdf returns 0.01, so p-value = 2 * 0.01 = 0.02 - // With pValPrec=6 => "0.020000" +test('computes p-value from the statistics module', async () => { + // Mocked studentTwoSidedPValue returns 0.02, with pValPrec=6 => "0.020000" render(); await waitFor(() => { From f9dfea87916712be0fa311a491abb9bfd635da51 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 17 Jul 2026 13:39:59 -0700 Subject: [PATCH 38/84] test(plugins): regression guards from the migration bug sweep Adds low-risk import-and-assert tests across migrated plugins, guarding the classes of bugs found while testing this branch: - is_timeseries charts must expose a datetime control (calendar, horizon, rose, partition). Partition genuinely lacked one, so add sections.legacyTimeseriesTime to its control panel (same 500 as paired-t-test otherwise). - deck.gl Multiple Layers must issue an empty query (self-fetches its layers). - deck.gl Arc is timeseries only when time_grain_sqla is set. - Time Pivot's numeric x-axis format must render a number, not the literal SMART_NUMBER string. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/controlPanel.test.ts | 23 +++++++++++ .../test/controlPanel.test.ts | 23 +++++++++++ .../src/controlPanel.tsx | 5 +++ .../test/controlPanel.test.ts | 26 +++++++++++++ .../test/controlPanel.test.ts | 26 +++++++++++++ .../src/Multi/buildQuery.test.ts | 37 ++++++++++++++++++ .../src/layers/Arc/buildQuery.test.ts | 38 +++++++++++++++++++ .../test/xAxisFormatter.test.ts | 30 +++++++++++++++ 8 files changed, 208 insertions(+) create mode 100644 superset-frontend/plugins/plugin-chart-calendar/test/controlPanel.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-horizon/test/controlPanel.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-partition/test/controlPanel.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-rose/test/controlPanel.test.ts create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.test.ts create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/buildQuery.test.ts create mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/xAxisFormatter.test.ts diff --git a/superset-frontend/plugins/plugin-chart-calendar/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/controlPanel.test.ts new file mode 100644 index 000000000000..4ce74c9c0d3a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-calendar/test/controlPanel.test.ts @@ -0,0 +1,23 @@ +/** + * 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 controlPanel from '../src/controlPanel'; + +test('exposes a datetime column control because the query is is_timeseries', () => { + expect(JSON.stringify(controlPanel)).toContain('granularity_sqla'); +}); diff --git a/superset-frontend/plugins/plugin-chart-horizon/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-horizon/test/controlPanel.test.ts new file mode 100644 index 000000000000..4ce74c9c0d3a --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-horizon/test/controlPanel.test.ts @@ -0,0 +1,23 @@ +/** + * 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 controlPanel from '../src/controlPanel'; + +test('exposes a datetime column control because the query is is_timeseries', () => { + expect(JSON.stringify(controlPanel)).toContain('granularity_sqla'); +}); diff --git a/superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx index c68b05babb94..927a3b9618db 100644 --- a/superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-partition/src/controlPanel.tsx @@ -27,11 +27,16 @@ import { D3_TIME_FORMAT_OPTIONS, DEFAULT_TIME_FORMAT, getStandardizedControls, + sections, } from '@superset-ui/chart-controls'; import OptionDescription from './OptionDescription'; const config: ControlPanelConfig = { controlPanelSections: [ + // buildQuery emits is_timeseries when a Time Series option is selected, so + // the query needs a datetime column. Without this section the chart 500s + // with "Datetime column not provided...". + sections.legacyTimeseriesTime, { label: t('Query'), expanded: true, diff --git a/superset-frontend/plugins/plugin-chart-partition/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-partition/test/controlPanel.test.ts new file mode 100644 index 000000000000..48be0948bbf1 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-partition/test/controlPanel.test.ts @@ -0,0 +1,26 @@ +/** + * 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 { sections } from '@superset-ui/chart-controls'; +import controlPanel from '../src/controlPanel'; + +test('includes the legacy timeseries Time section (buildQuery can be is_timeseries)', () => { + expect(controlPanel.controlPanelSections).toContain( + sections.legacyTimeseriesTime, + ); +}); diff --git a/superset-frontend/plugins/plugin-chart-rose/test/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-rose/test/controlPanel.test.ts new file mode 100644 index 000000000000..48be0948bbf1 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-rose/test/controlPanel.test.ts @@ -0,0 +1,26 @@ +/** + * 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 { sections } from '@superset-ui/chart-controls'; +import controlPanel from '../src/controlPanel'; + +test('includes the legacy timeseries Time section (buildQuery can be is_timeseries)', () => { + expect(controlPanel.controlPanelSections).toContain( + sections.legacyTimeseriesTime, + ); +}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.test.ts new file mode 100644 index 000000000000..82e7917f6c54 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/buildQuery.test.ts @@ -0,0 +1,37 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from './buildQuery'; + +const formData: QueryFormData = { + datasource: '1__table', + viz_type: 'deck_multi', + deck_slices: [1, 2], +}; + +test('deck.gl Multiple Layers issues no query of its own', () => { + // Each layer is a saved chart fetched client-side, so the composed chart must + // emit an empty queries array. That empty response is why metadata sets + // enableNoResults: false, else "No results were returned" hides the map. + expect(buildQuery(formData).queries).toEqual([]); +}); + +test('deck.gl Multiple Layers emits no query even with no layers selected', () => { + expect(buildQuery({ ...formData, deck_slices: [] }).queries).toEqual([]); +}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/buildQuery.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/buildQuery.test.ts new file mode 100644 index 000000000000..87c9da5484c0 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/buildQuery.test.ts @@ -0,0 +1,38 @@ +/** + * 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 buildQuery, { DeckArcFormData } from './buildQuery'; + +const baseFormData: DeckArcFormData = { + datasource: '1__table', + viz_type: 'deck_arc', + start_spatial: { type: 'latlong', lonCol: 'start_lon', latCol: 'start_lat' }, + end_spatial: { type: 'latlong', lonCol: 'end_lon', latCol: 'end_lat' }, +}; + +test('Arc buildQuery is not a timeseries query by default', () => { + expect(buildQuery(baseFormData).queries[0].is_timeseries).toBe(false); +}); + +test('Arc buildQuery is timeseries only when time_grain_sqla is set', () => { + const [query] = buildQuery({ + ...baseFormData, + time_grain_sqla: 'P1D', + }).queries; + expect(query.is_timeseries).toBe(true); +}); diff --git a/superset-frontend/plugins/preset-chart-nvd3/test/xAxisFormatter.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/xAxisFormatter.test.ts new file mode 100644 index 000000000000..70213a9c6b90 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-nvd3/test/xAxisFormatter.test.ts @@ -0,0 +1,30 @@ +/** + * 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 { getTimeFormatter } from '@superset-ui/core'; +import { getTimeOrNumberFormatter } from '../src/utils'; + +test('a number format renders a formatted number, not the literal name', () => { + // Time Pivot's x-axis defaults x_axis_format to SMART_NUMBER; NVD3Vis now + // routes it through getTimeOrNumberFormatter (number-aware) rather than + // getTimeFormatter, which would print the literal "SMART_NUMBER" every tick. + expect(getTimeOrNumberFormatter('SMART_NUMBER')(1950000)).toBe('1.95M'); + expect(getTimeFormatter('SMART_NUMBER')(new Date(Date.UTC(2024, 0, 1)))).toBe( + 'SMART_NUMBER', + ); +}); From d2911672bf9b75193f69f2e373d8f8f1316e48e2 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 14:05:14 -0700 Subject: [PATCH 39/84] fix(explore): honor chart enableNoResults metadata in ChartRenderer ChartRenderer computed the SuperChart enableNoResults prop purely from server-pagination + AG Grid filter state (bypassNoResult) and never read the chart plugin's own enableNoResults metadata. Charts that fetch their own data and issue no top-level query, like deck.gl Multiple Layers, set enableNoResults: false but still hit the "No results were returned for this query" empty state in explore, which pre-empts their self-fetching renderer from ever mounting. AND the chart's enableNoResults metadata (defaulting to true) into bypassNoResult so deck_multi and similar self-fetching charts render. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/Chart/ChartRenderer.test.tsx | 28 +++++++++++++++++++ .../src/components/Chart/ChartRenderer.tsx | 16 ++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx index 672a3995c7b6..2b491583dff7 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx @@ -46,6 +46,7 @@ jest.mock('@superset-ui/core', () => ({

{JSON.stringify(postTransformProps(props).formData)}
@@ -147,6 +148,33 @@ test('should not render chart context menu if the context menu is suppressed for expect(queryByTestId('mock-chart-context-menu')).not.toBeInTheDocument(); }); +test('enables the no-results state by default for a chart without enableNoResults metadata', () => { + const { getByTestId } = render(); + expect(getByTestId('mock-super-chart')).toHaveAttribute( + 'data-enable-no-results', + 'true', + ); +}); + +test('honors enableNoResults: false from the chart metadata (e.g. self-fetching charts like deck.gl Multiple Layers)', () => { + getChartMetadataRegistry().registerValue( + 'chart_without_no_results', + new ChartMetadata({ + name: 'chart that fetches its own data', + thumbnail: '.png', + useLegacyApi: false, + enableNoResults: false, + }), + ); + const { getByTestId } = render( + , + ); + expect(getByTestId('mock-super-chart')).toHaveAttribute( + 'data-enable-no-results', + 'false', + ); +}); + test('should detect changes in matrixify properties', () => { const initialProps: ChartRendererProps = { ...requiredProps, diff --git a/superset-frontend/src/components/Chart/ChartRenderer.tsx b/superset-frontend/src/components/Chart/ChartRenderer.tsx index 3c6ebab63d8e..59cbb1779c39 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.tsx @@ -499,10 +499,18 @@ function ChartRendererComponent({ Object.keys(ownState.agGridFilterModel).length > 0; const currentFormDataExtended = currentFormData as JsonObject; - const bypassNoResult = !( - currentFormDataExtended?.server_pagination && - (hasSearchText || hasAgGridFilters) - ); + // Some charts fetch their own data and issue no top-level query (e.g. deck.gl + // Multiple Layers, which renders its sub-layer charts), so an empty response + // is expected and shouldn't trigger the no-results state. Honor the chart's + // own enableNoResults metadata (defaults to true). + const chartEnableNoResults = + getChartMetadataRegistry().get(vizType)?.enableNoResults ?? true; + const bypassNoResult = + chartEnableNoResults && + !( + currentFormDataExtended?.server_pagination && + (hasSearchText || hasAgGridFilters) + ); return ( <> From f93cc1a67b223a51cdde83cd52fbe455c6828abf Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 15:32:42 -0700 Subject: [PATCH 40/84] test(paired-t-test): prove the v1 pipeline renders table rows end-to-end Guards the "headers render but no body rows" failure mode by driving the real transformProps -> PairedTTest pipeline with a realistic flat /api/v1/chart/data timeseries response (main.birth_names shape): asserts transformProps keys the reshaped data by metric label with one series per group tuple, and that the rendered tables contain a body row per group, including when optional precision/significance controls are absent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/pipeline.test.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx new file mode 100644 index 000000000000..3957412676cd --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx @@ -0,0 +1,97 @@ +/** + * 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 { render } from 'spec/helpers/testing-library'; +import transformProps from '../src/transformProps'; +import PairedTTest from '../src/PairedTTest'; + +// Deterministic p-value so significance/formatting are stable. +jest.mock('../src/statistics', () => ({ + __esModule: true, + studentTwoSidedPValue: () => 0.02, +})); + +// A realistic /api/v1/chart/data timeseries response for main.birth_names: +// groupby=[gender], two adhoc SUM metrics, epoch-ms __timestamp, 44 years. +const adhoc = (col: string) => ({ + expressionType: 'SIMPLE', + column: { column_name: col }, + aggregate: 'SUM', + label: `SUM(${col})`, +}); +const rows: Record[] = []; +for (let y = 0; y < 44; y += 1) { + const ts = Date.UTC(1965 + y, 0, 1); + rows.push({ + __timestamp: ts, + gender: 'boy', + 'SUM(num_boys)': 1000000 + y, + 'SUM(num_girls)': 0, + }); + rows.push({ + __timestamp: ts, + gender: 'girl', + 'SUM(num_boys)': 0, + 'SUM(num_girls)': 900000 + y, + }); +} + +const buildProps = (formDataExtra: Record = {}) => + transformProps({ + formData: { + groupby: ['gender'], + metrics: [adhoc('num_boys'), adhoc('num_girls')], + significanceLevel: 0.05, + liftvaluePrecision: '4', + pvaluePrecision: '6', + ...formDataExtra, + }, + queriesData: [{ data: rows }], + } as any) as any; + +test('transformProps reshapes flat v1 records into per-metric series keyed by metric label', () => { + const props = buildProps(); + expect(props.metrics).toEqual(['SUM(num_boys)', 'SUM(num_girls)']); + expect(Object.keys(props.data)).toEqual(['SUM(num_boys)', 'SUM(num_girls)']); + // one series per group tuple (boy / girl) + expect(props.data['SUM(num_boys)']).toHaveLength(2); + expect(props.data['SUM(num_boys)'].map((s: any) => s.group)).toEqual([ + ['boy'], + ['girl'], + ]); +}); + +test('the full pipeline renders a body row per group for each metric table', () => { + const { container } = render(); + // two metric tables, each with two group rows (boy / girl) + const bodyRows = container.querySelectorAll('tbody tr'); + expect(bodyRows).toHaveLength(4); + expect(container.querySelectorAll('td[label="gender"]')).toHaveLength(4); +}); + +test('renders rows even when precision/significance controls are absent from formData', () => { + // Guards the "headers render but no rows" failure mode: missing optional + // controls must not blank the table body. + const props = buildProps({ + significanceLevel: undefined, + liftvaluePrecision: undefined, + pvaluePrecision: undefined, + }); + const { container } = render(); + expect(container.querySelectorAll('tbody tr')).toHaveLength(4); +}); From feb224686b1dac31d5203a58ba93f32cf01614d2 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 17 Jul 2026 15:33:16 -0700 Subject: [PATCH 41/84] fix(tests): drop removed useLegacyApi prop from ChartRenderer test metadata The legacy-pipeline removal deleted useLegacyApi from ChartMetadataConfig; the enableNoResults test still passed it, breaking tsc in lint-frontend. Co-Authored-By: Claude Opus 4.8 --- superset-frontend/src/components/Chart/ChartRenderer.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx index 2b491583dff7..83eb3915172a 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.test.tsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.test.tsx @@ -162,7 +162,6 @@ test('honors enableNoResults: false from the chart metadata (e.g. self-fetching new ChartMetadata({ name: 'chart that fetches its own data', thumbnail: '.png', - useLegacyApi: false, enableNoResults: false, }), ); From ac7f53326f583442bbe47ebc07838a94f7cd9ae8 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 18:13:38 -0700 Subject: [PATCH 42/84] fix(paired-t-test): render the table without the reactable dependency The migrated chart returned correct data (verified end-to-end) yet showed table headers with no body rows in production builds, while every jest path rendered rows. The common factor was reactable, an unmaintained 2016 library that relies on React-18-incompatible legacy lifecycles (componentWillMount/componentWillReceiveProps) and identifies its row children by stringifying components -- fragile in a production bundle. Replace it with a plain semantic table that reproduces every behavior (control-row selection, per-cell lift/p-value/significance coloring, and the same column sort comparators), reusing the existing CSS class hooks so the surrounding styles are unchanged. Rendering is now deterministic and fully unit-testable. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/TTestTable.tsx | 292 +++++++++--------- .../test/pipeline.test.tsx | 3 +- 2 files changed, 149 insertions(+), 146 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx index 7cbf834f5c61..bbb904d3bc61 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx @@ -17,9 +17,8 @@ * under the License. */ /* eslint-disable react/no-array-index-key, react/jsx-no-bind */ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef, KeyboardEvent } from 'react'; import { studentTwoSidedPValue } from './statistics'; -import { Table, Tr, Td, Thead, Th } from 'reactable'; interface DataPointValue { x: number; @@ -40,6 +39,58 @@ interface TTestTableProps { pValPrec?: number; } +const P_VALUE = 'pValue'; +const LIFT_VALUE = 'liftValue'; +const SIGNIFICANT = 'significant'; + +type Comparator = (a: string, b: string) => number; + +// Column comparators ported from the previous reactable `sortable` config. +// 'control' always sorts to the top in ascending order (the table toggles to +// descending on a second click, mirroring the old behavior). +const COMPARATORS: Record = { + [P_VALUE]: (a, b) => { + if (a === 'control') return -1; + if (b === 'control') return 1; + if (a === b) return 0; + return a > b ? 1 : -1; // p-values ascending + }, + [LIFT_VALUE]: (a, b) => { + if (a === 'control') return -1; + if (b === 'control') return 1; + const liftA = parseFloat(a); + const liftB = parseFloat(b); + const aFinite = Number.isFinite(liftA); + const bFinite = Number.isFinite(liftB); + // Non-finite (Infinity/NaN) lift values sort ahead of finite ones, + // consistently regardless of comparison order, to avoid an antisymmetric + // comparator. + if (!aFinite && !bFinite) return 0; + if (!aFinite) return -1; + if (!bFinite) return 1; + if (liftA === liftB) return 0; + return liftA > liftB ? -1 : 1; // lift values descending + }, + [SIGNIFICANT]: (a, b) => { + if (a === 'control') return -1; + if (b === 'control') return 1; + if (a === b) return 0; + return a > b ? -1 : 1; // significant values first + }, +}; + +// Default comparator for the group columns: numeric when both values parse as +// numbers, lexicographic otherwise. +const defaultCompare: Comparator = (a, b) => { + const na = Number(a); + const nb = Number(b); + if (a !== '' && b !== '' && !Number.isNaN(na) && !Number.isNaN(nb)) { + return na - nb; + } + if (a === b) return 0; + return a < b ? -1 : 1; +}; + function TTestTable({ alpha = 0.05, data, @@ -54,6 +105,8 @@ function TTestTable({ const controlRef = useRef(0); const [liftValues, setLiftValues] = useState<(string | number)[]>([]); const [pValues, setPValues] = useState<(string | number)[]>([]); + const [sortColumn, setSortColumn] = useState(null); + const [sortDesc, setSortDesc] = useState(false); const computeLift = useCallback( (values: DataPointValue[], controlValues: DataPointValue[]): string => { @@ -207,160 +260,109 @@ function TTestTable({ [computeTTest], ); - // When sorted ascending, 'control' will always be at top - type SortConfigItem = - string | { column: string; sortFunction: (a: string, b: string) => number }; - - const sortConfig: SortConfigItem[] = useMemo( - () => - (groups as SortConfigItem[]).concat([ - { - column: 'pValue', - sortFunction: (a: string, b: string) => { - if (a === 'control') { - return -1; - } - if (b === 'control') { - return 1; - } - if (a === b) { - return 0; - } - - return a > b ? 1 : -1; // p-values ascending - }, - }, - { - column: 'liftValue', - sortFunction: (a: string, b: string) => { - if (a === 'control') { - return -1; - } - if (b === 'control') { - return 1; - } - - const liftA = parseFloat(a); - const liftB = parseFloat(b); - const aFinite = Number.isFinite(liftA); - const bFinite = Number.isFinite(liftB); - // Non-finite (Infinity/NaN) lift values sort ahead of finite ones, - // consistently regardless of comparison order, to avoid an - // antisymmetric comparator - if (!aFinite && !bFinite) { - return 0; - } - if (!aFinite) { - return -1; - } - if (!bFinite) { - return 1; - } - if (liftA === liftB) { - return 0; - } - - return liftA > liftB ? -1 : 1; // lift values descending - }, - }, - { - column: 'significant', - sortFunction: (a: string, b: string) => { - if (a === 'control') { - return -1; - } - if (b === 'control') { - return 1; - } - if (a === b) { - return 0; - } - - return a > b ? -1 : 1; // significant values first - }, - }, - ]), - [groups], - ); + const handleSort = useCallback((column: string) => { + setSortColumn(prev => { + if (prev === column) { + setSortDesc(desc => !desc); + return prev; + } + setSortDesc(false); + return column; + }); + }, []); if (!Array.isArray(groups) || groups.length === 0) { throw new Error('Group by param is required'); } - // Render column header for each group - const columns = groups.map((group, i) => ( - - {group} - - )); - const numGroups = groups.length; - // Columns for p-value, lift-value, and significance (true/false) - columns.push( - - p-value - , - ); - columns.push( - - Lift % - , - ); - columns.push( - - Significant - , - ); + const columns = [ + ...groups.map(group => ({ key: group, label: group })), + { key: P_VALUE, label: 'p-value' }, + { key: LIFT_VALUE, label: 'Lift %' }, + { key: SIGNIFICANT, label: 'Significant' }, + ]; - const rows = (data ?? []).map((entry, i) => { - const values = groups.map( - ( - group, - j, // group names - ) => , - ); - values.push( - , - ); - values.push( - , - ); - values.push( - , - ); + // Value used to sort a row by the active column. + const sortValueOf = (index: number): string => { + if (sortColumn === null) { + return ''; + } + const groupIndex = groups.indexOf(sortColumn); + if (groupIndex >= 0) { + return String(data[index].group[groupIndex]); + } + if (sortColumn === P_VALUE) { + return String(pValues[index]); + } + if (sortColumn === LIFT_VALUE) { + return String(liftValues[index]); + } + return String(getSignificance(index)); + }; - return ( - handleRowClick(i)} - > - {values} - - ); - }); + const rowOrder = (data ?? []).map((_, i) => i); + if (sortColumn !== null) { + const compare = COMPARATORS[sortColumn] ?? defaultCompare; + rowOrder.sort((a, b) => { + const result = compare(sortValueOf(a), sortValueOf(b)); + return sortDesc ? -result : result; + }); + } return (

{metric}

- - {columns} - {rows} -
+ + + + {columns.map(column => { + const sortClass = + sortColumn === column.key + ? sortDesc + ? ' reactable-header-sort-desc' + : ' reactable-header-sort-asc' + : ''; + return ( + + ); + })} + + + + {rowOrder.map(i => { + const entry = data[i]; + const significance = getSignificance(i); + return ( + handleRowClick(i)} + > + {groups.map((group, j) => ( + + ))} + + + + + ); + })} + +
handleSort(column.key)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleSort(column.key); + } + }} + > + {column.label} +
{entry.group[j]}{pValues[i]}{liftValues[i]}{String(significance)}
); } diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx index 3957412676cd..c6ce820f1d9b 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/pipeline.test.tsx @@ -81,7 +81,8 @@ test('the full pipeline renders a body row per group for each metric table', () // two metric tables, each with two group rows (boy / girl) const bodyRows = container.querySelectorAll('tbody tr'); expect(bodyRows).toHaveLength(4); - expect(container.querySelectorAll('td[label="gender"]')).toHaveLength(4); + // each row leads with its group value; boy/girl appears once per table + expect(container.querySelectorAll('tbody tr td:first-child')).toHaveLength(4); }); test('renders rows even when precision/significance controls are absent from formData', () => { From 9ff15cb53e922700196ab8018d5ea3ff62ba6834 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 19:33:46 -0700 Subject: [PATCH 43/84] fix(paired-t-test): scroll the chart when tables overflow its height The container styled the non-existent .scrollbar-container class, so the chart never scrolled and tall output (many groups across multiple metrics) was clipped with no way to reach the rest. Make the chart root fill its allotted height and scroll on overflow. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx index fd339dfb1645..68a47d1065d1 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx @@ -33,9 +33,10 @@ interface PairedTTestProps { const StyledDiv = styled.div` ${({ theme }) => ` - .superset-legacy-chart-paired-t-test .scrollbar-container { - overflow: auto; - } + /* Fill the chart's allotted height and scroll when the tables overflow */ + /* it (e.g. many groups across multiple metrics). */ + height: 100%; + overflow: auto; .paired-ttest-table .scrollbar-content { padding-left: ${theme.sizeUnit}px; From a82128cde06fc60ba3f006c15691e9977277a657 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 20:03:32 -0700 Subject: [PATCH 44/84] refactor(paired-t-test): use the antd Table and drop the reactable dependency Replace the interim plain-table rendering with the design-system antd Table from @superset-ui/core/components: native column sorting via the ported comparators, per-cell lift/p-value/significance coloring through semantic theme tokens (no bespoke CSS classes), and row-click control selection via onRow. Removes the reactable dependency entirely (its fragile, React-18- incompatible rendering was the production bug) and the leftover reactable-* styling. Also drops the now-stale distributions/cephes lockfile entries. Co-Authored-By: Claude Opus 4.8 (1M context) --- superset-frontend/package-lock.json | 28 +-- .../plugin-chart-paired-t-test/package.json | 3 +- .../src/PairedTTest.tsx | 103 ++------- .../src/TTestTable.tsx | 211 +++++++++--------- .../test/TTestTable.test.tsx | 173 +++++--------- 5 files changed, 185 insertions(+), 333 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index f82be3bd74f8..c09aab93727b 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -16533,12 +16533,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/cephes": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/cephes/-/cephes-2.0.0.tgz", - "integrity": "sha512-4GMUzkcXHZ0HMZ3gZdBrv8pQs1/zkJh2Q9rQOF8NJZHanM359y3XOSdeqmDBPfxQKYQpJt58R3dUpofrIXJ2mg==", - "license": "BSD-3-Clause" - }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -18956,15 +18950,6 @@ "readable-stream": "^3.1.1" } }, - "node_modules/distributions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/distributions/-/distributions-2.2.0.tgz", - "integrity": "sha512-n7ybud+CRAOZlpg+ETuA0PTiSBfyVNt8Okns5gSK4NvHwj7RamQoufptOucvVcTn9CV4DZ38p1k6TgwMexUNkQ==", - "license": "MIT", - "dependencies": { - "cephes": "^2.0.0" - } - }, "node_modules/dnd-core": { "version": "11.1.3", "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-11.1.3.tgz", @@ -36050,15 +36035,6 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, - "node_modules/reactable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reactable/-/reactable-1.1.0.tgz", - "integrity": "sha512-SnvZ3CXyFFxGotw9cqNiVUGb2oW16UlIypGQZRJGgPiJuFqW22jO7A+Y/Tvv8no8F/bZoLdZ+QJP7eZfcc9kCw==", - "license": "MIT", - "peerDependencies": { - "react": "* || ^0.14.0" - } - }, "node_modules/read": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/read/-/read-4.1.0.tgz", @@ -44465,9 +44441,7 @@ "version": "0.20.3", "license": "Apache-2.0", "dependencies": { - "distributions": "^2.2.0", - "prop-types": "^15.8.1", - "reactable": "^1.1.0" + "prop-types": "^15.8.1" }, "peerDependencies": { "@apache-superset/core": "*", diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json index 10de58072d1a..5810f252e5a0 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json @@ -23,8 +23,7 @@ "lib" ], "dependencies": { - "prop-types": "^15.8.1", - "reactable": "^1.1.0" + "prop-types": "^15.8.1" }, "peerDependencies": { "@superset-ui/chart-controls": "*", diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx index 68a47d1065d1..391e7f834840 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx @@ -33,77 +33,24 @@ interface PairedTTestProps { const StyledDiv = styled.div` ${({ theme }) => ` - /* Fill the chart's allotted height and scroll when the tables overflow */ - /* it (e.g. many groups across multiple metrics). */ + /* Fill the chart's allotted height and scroll when the tables overflow it + (e.g. many groups across multiple metrics). */ height: 100%; overflow: auto; + padding: 0 ${theme.sizeUnit}px; - .paired-ttest-table .scrollbar-content { - padding-left: ${theme.sizeUnit}px; - padding-right: ${theme.sizeUnit}px; - margin-bottom: 0; - } - - .paired-ttest-table table { - margin-bottom: 0; - } - - .paired-ttest-table h1 { + h3 { margin-left: ${theme.sizeUnit}px; } - .reactable-data tr { - font-feature-settings: 'tnum' 1; - } - - .reactable-data tr, - .reactable-header-sortable { - -webkit-transition: ease-in-out 0.1s; - transition: ease-in-out 0.1s; - } - - .reactable-data tr:hover { - background-color: ${theme.colorFillTertiary}; - } - - .reactable-data tr .false { - color: ${theme.colorError}; - } - - .reactable-data tr .true { - color: ${theme.colorSuccess}; - } - - .reactable-data tr .control { - color: ${theme.colorPrimary}; - } - - .reactable-data tr .invalid { - color: ${theme.colorWarning}; - } - - .reactable-data .control td { - background-color: ${theme.colorFillTertiary}; - } - - .reactable-header-sortable:hover, - .reactable-header-sortable:focus, - .reactable-header-sort-asc, - .reactable-header-sort-desc { - background-color: ${theme.colorFillTertiary}; - position: relative; + /* Space the per-metric tables apart */ + & > div + div { + margin-top: ${theme.sizeUnit * 2}px; } - .reactable-header-sort-asc:after { - content: '\\25bc'; - position: absolute; - right: ${theme.sizeUnit * 3}px; - } - - .reactable-header-sort-desc:after { - content: '\\25b2'; - position: absolute; - right: ${theme.sizeUnit * 3}px; + /* Keep the numeric columns aligned with tabular figures */ + td { + font-feature-settings: 'tnum' 1; } `} `; @@ -118,24 +65,18 @@ function PairedTTest({ pValPrec = 6, }: PairedTTestProps) { return ( - -
-
-
- {metrics.map((metric, i) => ( - - ))} -
-
-
+ + {metrics.map((metric, i) => ( + + ))} ); } diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx index bbb904d3bc61..7a26544f204c 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/TTestTable.tsx @@ -16,8 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint-disable react/no-array-index-key, react/jsx-no-bind */ -import { useState, useEffect, useCallback, useRef, KeyboardEvent } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { useTheme } from '@apache-superset/core/theme'; +import { + Table, + TableSize, + type ColumnsType, +} from '@superset-ui/core/components/Table'; import { studentTwoSidedPValue } from './statistics'; interface DataPointValue { @@ -39,6 +44,14 @@ interface TTestTableProps { pValPrec?: number; } +type TTestRow = { + key: number; + index: number; + pValue: string | number; + liftValue: string | number; + significant: string; +} & Record; + const P_VALUE = 'pValue'; const LIFT_VALUE = 'liftValue'; const SIGNIFICANT = 'significant'; @@ -46,8 +59,7 @@ const SIGNIFICANT = 'significant'; type Comparator = (a: string, b: string) => number; // Column comparators ported from the previous reactable `sortable` config. -// 'control' always sorts to the top in ascending order (the table toggles to -// descending on a second click, mirroring the old behavior). +// 'control' always sorts to the top in ascending order. const COMPARATORS: Record = { [P_VALUE]: (a, b) => { if (a === 'control') return -1; @@ -99,14 +111,13 @@ function TTestTable({ metric, pValPrec = 6, }: TTestTableProps) { + const theme = useTheme(); const [control, setControl] = useState(0); // Mirrors `control` so the data-change effect can read the latest value // without re-running (and recomputing) after every row click const controlRef = useRef(0); const [liftValues, setLiftValues] = useState<(string | number)[]>([]); const [pValues, setPValues] = useState<(string | number)[]>([]); - const [sortColumn, setSortColumn] = useState(null); - const [sortDesc, setSortDesc] = useState(false); const computeLift = useCallback( (values: DataPointValue[], controlValues: DataPointValue[]): string => { @@ -155,7 +166,7 @@ function TTestTable({ try { // two-sided test return studentTwoSidedPValue(tvalue, finiteCount - 1).toFixed(pValPrec); - } catch (error) { + } catch { return NaN; } }, @@ -209,12 +220,10 @@ function TTestTable({ const getLiftStatus = useCallback( (row: number): string => { - // Get a css class name for coloring if (row === control) { return 'control'; } - const liftVal = liftValues[row]; - const numericLiftVal = Number(liftVal); + const numericLiftVal = Number(liftValues[row]); if (Number.isNaN(numericLiftVal) || !Number.isFinite(numericLiftVal)) { return 'invalid'; // infinite or NaN values } @@ -229,8 +238,7 @@ function TTestTable({ if (row === control) { return 'control'; } - const pVal = pValues[row]; - const numericPVal = Number(pVal); + const numericPVal = Number(pValues[row]); if (Number.isNaN(numericPVal) || !Number.isFinite(numericPVal)) { return 'invalid'; } @@ -242,7 +250,6 @@ function TTestTable({ const getSignificance = useCallback( (row: number): string | boolean => { - // Color significant as green, else red if (row === control) { return 'control'; } @@ -260,109 +267,105 @@ function TTestTable({ [computeTTest], ); - const handleSort = useCallback((column: string) => { - setSortColumn(prev => { - if (prev === column) { - setSortDesc(desc => !desc); - return prev; - } - setSortDesc(false); - return column; - }); - }, []); - if (!Array.isArray(groups) || groups.length === 0) { throw new Error('Group by param is required'); } - const columns = [ - ...groups.map(group => ({ key: group, label: group })), - { key: P_VALUE, label: 'p-value' }, - { key: LIFT_VALUE, label: 'Lift %' }, - { key: SIGNIFICANT, label: 'Significant' }, - ]; - - // Value used to sort a row by the active column. - const sortValueOf = (index: number): string => { - if (sortColumn === null) { - return ''; - } - const groupIndex = groups.indexOf(sortColumn); - if (groupIndex >= 0) { - return String(data[index].group[groupIndex]); - } - if (sortColumn === P_VALUE) { - return String(pValues[index]); + // Map a status keyword to its theme color; the control/significant/lift + // states reuse the semantic antd tokens. + const statusColor = (status: string): string | undefined => { + switch (status) { + case 'control': + return theme.colorPrimary; + case 'true': + return theme.colorSuccess; + case 'false': + return theme.colorError; + case 'invalid': + return theme.colorWarning; + default: + return undefined; } - if (sortColumn === LIFT_VALUE) { - return String(liftValues[index]); - } - return String(getSignificance(index)); }; - const rowOrder = (data ?? []).map((_, i) => i); - if (sortColumn !== null) { - const compare = COMPARATORS[sortColumn] ?? defaultCompare; - rowOrder.sort((a, b) => { - const result = compare(sortValueOf(a), sortValueOf(b)); - return sortDesc ? -result : result; + const rows: TTestRow[] = (data ?? []).map((entry, i) => { + const row = { + key: i, + index: i, + pValue: pValues[i], + liftValue: liftValues[i], + significant: String(getSignificance(i)), + } as TTestRow; + groups.forEach((group, j) => { + row[group] = entry.group[j]; }); - } + return row; + }); + + const columns: ColumnsType = [ + ...groups.map(group => ({ + title: group, + dataIndex: group, + key: group, + sorter: (a: TTestRow, b: TTestRow) => + defaultCompare(String(a[group]), String(b[group])), + })), + { + title: 'p-value', + dataIndex: P_VALUE, + key: P_VALUE, + sorter: (a: TTestRow, b: TTestRow) => + COMPARATORS[P_VALUE](String(a.pValue), String(b.pValue)), + render: (value: string | number, row: TTestRow) => ( + + {value} + + ), + }, + { + title: 'Lift %', + dataIndex: LIFT_VALUE, + key: LIFT_VALUE, + sorter: (a: TTestRow, b: TTestRow) => + COMPARATORS[LIFT_VALUE](String(a.liftValue), String(b.liftValue)), + render: (value: string | number, row: TTestRow) => ( + + {value} + + ), + }, + { + title: 'Significant', + dataIndex: SIGNIFICANT, + key: SIGNIFICANT, + sorter: (a: TTestRow, b: TTestRow) => + COMPARATORS[SIGNIFICANT](String(a.significant), String(b.significant)), + render: (value: string) => ( + {value} + ), + }, + ]; return (

{metric}

- - - - {columns.map(column => { - const sortClass = - sortColumn === column.key - ? sortDesc - ? ' reactable-header-sort-desc' - : ' reactable-header-sort-asc' - : ''; - return ( - - ); - })} - - - - {rowOrder.map(i => { - const entry = data[i]; - const significance = getSignificance(i); - return ( - handleRowClick(i)} - > - {groups.map((group, j) => ( - - ))} - - - - - ); - })} - -
handleSort(column.key)} - onKeyDown={(e: KeyboardEvent) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - handleSort(column.key); - } - }} - > - {column.label} -
{entry.group[j]}{pValues[i]}{liftValues[i]}{String(significance)}
+ + data={rows} + columns={columns} + size={TableSize.Small} + rowKey="key" + usePagination={false} + sticky={false} + onRow={row => ({ + onClick: () => handleRowClick(row.index), + style: { + cursor: 'pointer', + ...(row.index === control + ? { backgroundColor: theme.colorFillTertiary } + : {}), + }, + })} + />
); } diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx index fbaa8126d498..9dc0145565e2 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/test/TTestTable.test.tsx @@ -16,8 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { render } from 'spec/helpers/testing-library'; +import { supersetTheme } from '@apache-superset/core/theme'; import TTestTable from '../src/TTestTable'; import type { DataEntry } from '../src/TTestTable'; @@ -54,24 +56,20 @@ const defaultProps = { pValPrec: 6, }; +const dataRows = () => + document.querySelectorAll('tr.ant-table-row'); + test('renders the metric name as an h3 heading', async () => { render(); - await waitFor(() => { - expect(screen.getByText('revenue')).toBeInTheDocument(); - }); - - const heading = screen.getByText('revenue'); + const heading = await screen.findByText('revenue'); expect(heading.tagName).toBe('H3'); }); test('renders a table with the correct column headers', async () => { render(); - await waitFor(() => { - expect(screen.getByRole('table')).toBeInTheDocument(); - }); - + await waitFor(() => expect(dataRows()).toHaveLength(2)); expect(screen.getByText('category')).toBeInTheDocument(); expect(screen.getByText('p-value')).toBeInTheDocument(); expect(screen.getByText('Lift %')).toBeInTheDocument(); @@ -80,14 +78,8 @@ test('renders a table with the correct column headers', async () => { test('renders group columns matching the groups prop', async () => { const multiGroupData: DataEntry[] = [ - { - group: ['group-A', 'sub-1'], - values: [{ x: 1, y: 10 }], - }, - { - group: ['group-B', 'sub-2'], - values: [{ x: 1, y: 15 }], - }, + { group: ['group-A', 'sub-1'], values: [{ x: 1, y: 10 }] }, + { group: ['group-B', 'sub-2'], values: [{ x: 1, y: 15 }] }, ]; render( @@ -98,65 +90,52 @@ test('renders group columns matching the groups prop', async () => { />, ); - await waitFor(() => { - expect(screen.getByRole('table')).toBeInTheDocument(); - }); - + await waitFor(() => expect(dataRows()).toHaveLength(2)); expect(screen.getByText('category')).toBeInTheDocument(); expect(screen.getByText('subcategory')).toBeInTheDocument(); }); -test('first row is treated as control by default and shows "control" for p-value and lift columns', async () => { +test('first row is treated as control by default and shows "control" for the computed columns', async () => { render(); - // After componentDidMount, the first row should be control + // The control row renders "control" in its p-value, lift and significance cells await waitFor(() => { - const controlTexts = screen.getAllByText('control'); - // The control row has "control" in pValue and liftValue columns - expect(controlTexts.length).toBeGreaterThanOrEqual(2); + expect(screen.getAllByText('control').length).toBeGreaterThanOrEqual(2); }); }); test('computes lift values correctly for non-control rows', async () => { - // Control (group-A): sum of y = 10 + 20 = 30 - // group-B: sum of y = 15 + 25 = 40 - // Lift = ((40 - 30) / 30) * 100 = 33.3333% - // With liftValPrec=4 => "33.3333" + // Control (group-A): sum of y = 30, group-B: sum of y = 40 + // Lift = ((40 - 30) / 30) * 100 = 33.3333 with liftValPrec=4 render(); - await waitFor(() => { - expect(screen.getByText('33.3333')).toBeInTheDocument(); - }); + expect(await screen.findByText('33.3333')).toBeInTheDocument(); }); test('computes p-value from the statistics module', async () => { // Mocked studentTwoSidedPValue returns 0.02, with pValPrec=6 => "0.020000" render(); - await waitFor(() => { - expect(screen.getByText('0.020000')).toBeInTheDocument(); - }); + expect(await screen.findByText('0.020000')).toBeInTheDocument(); }); test('marks non-control row as significant when p-value is below alpha', async () => { - // p-value = 0.02 < alpha = 0.05, so significance is true + // p-value 0.02 < alpha 0.05, so significance is true render(); - await waitFor(() => { - expect(screen.getByText('true')).toBeInTheDocument(); - }); + const cell = await screen.findByText('true'); + expect(cell).toHaveStyle({ color: supersetTheme.colorSuccess }); }); test('marks non-control row as not significant when p-value is above alpha', async () => { - // p-value = 0.02 > alpha = 0.01, so significance is false + // p-value 0.02 > alpha 0.01, so significance is false render(); - await waitFor(() => { - expect(screen.getByText('false')).toBeInTheDocument(); - }); + const cell = await screen.findByText('false'); + expect(cell).toHaveStyle({ color: supersetTheme.colorError }); }); -test('renders Infinity lift when control values sum to zero', async () => { +test('renders Infinity lift when control values sum to zero, colored as invalid', async () => { const zeroControlData: DataEntry[] = [ { group: ['zero-group'], @@ -176,24 +155,16 @@ test('renders Infinity lift when control values sum to zero', async () => { render(); - // The lift computation: ((sumValues - sumControl) / sumControl) * 100 - // = ((30 - 0) / 0) * 100 = Infinity - // Infinity.toFixed(4) returns "Infinity", and the component renders it. - // getLiftStatus classifies this as "invalid" (non-finite). - await waitFor(() => { - expect(screen.getByText('Infinity')).toBeInTheDocument(); - }); - expect(screen.getByText('Infinity')).toHaveClass('invalid'); + // ((30 - 0) / 0) * 100 = Infinity; getLiftStatus classifies non-finite as invalid + const cell = await screen.findByText('Infinity'); + expect(cell).toHaveStyle({ color: supersetTheme.colorWarning }); }); -test('colors finite lift cells with true/false status classes', async () => { +test('colors finite positive lift cells with the success token', async () => { render(); - // group-B lift vs control group-A: ((40 - 30) / 30) * 100 = 33.3333 (>= 0) - await waitFor(() => { - expect(screen.getByText('33.3333')).toBeInTheDocument(); - }); - expect(screen.getByText('33.3333')).toHaveClass('true'); + const cell = await screen.findByText('33.3333'); + expect(cell).toHaveStyle({ color: supersetTheme.colorSuccess }); }); test('recomputes and clamps the control index when data shrinks', async () => { @@ -207,35 +178,28 @@ test('recomputes and clamps the control index when data shrinks', async () => { ); // Make the last row (index 2) the control - await waitFor(() => { - expect(screen.getByText('row-2')).toBeInTheDocument(); - }); - fireEvent.click(screen.getByText('row-2').closest('tr')!); + const row2 = await screen.findByText('row-2'); + fireEvent.click(row2.closest('tr')!); // row-0 lift vs row-2 control: ((10 - 40) / 40) * 100 = -75.0000 - await waitFor(() => { - expect(screen.getByText('-75.0000')).toBeInTheDocument(); - }); + expect(await screen.findByText('-75.0000')).toBeInTheDocument(); // Shrink the data so the control index (2) is out of range rerender(); // Control clamps to the last remaining row (index 1) and values recompute: // row-0 lift vs row-1 control: ((10 - 20) / 20) * 100 = -50.0000 - await waitFor(() => { - expect(screen.getByText('-50.0000')).toBeInTheDocument(); - }); + expect(await screen.findByText('-50.0000')).toBeInTheDocument(); expect(screen.queryByText('row-2')).not.toBeInTheDocument(); }); test('sorts a non-finite lift value deterministically regardless of comparison order', async () => { - // Control sum is non-zero (finite denominator), so row-normal gets a - // finite lift while row-huge overflows to a non-finite ("Infinity") lift, - // exercising a mixed finite/non-finite comparison. + // Control sum is non-zero, so row-normal gets a finite lift while row-huge + // overflows to a non-finite ("Infinity") lift. const mixedLiftData: DataEntry[] = [ { group: ['control'], values: [{ x: 1, y: 100 }] }, { group: ['row-normal'], values: [{ x: 1, y: 200 }] }, - { group: ['row-huge'], values: [{ x: 1, y: 1e309 }] }, + { group: ['row-huge'], values: [{ x: 1, y: Infinity }] }, ]; render( @@ -250,13 +214,9 @@ test('sorts a non-finite lift value deterministically regardless of comparison o fireEvent.click(screen.getByText('Lift %')); await waitFor(() => { - const rowLabels = screen - .getAllByRole('row') - .slice(1) // drop the header row - .map(row => row.textContent); - // Sorting is antisymmetric and stable: the same relative order results - // whichever pair of rows is compared first, with the non-finite value - // consistently ordered ahead of the finite one. + const rowLabels = Array.from(dataRows()).map(row => row.textContent); + // Non-finite value is consistently ordered ahead of the finite one, with + // the control pinned to the top. expect(rowLabels).toEqual([ expect.stringContaining('control'), expect.stringContaining('row-huge'), @@ -279,39 +239,22 @@ test('throws an error when groups array is empty', () => { test('clicking a non-control row changes it to the new control', async () => { render(); - // Wait for initial render with group-A as control - await waitFor(() => { - expect(screen.getByText('group-A')).toBeInTheDocument(); - expect(screen.getByText('group-B')).toBeInTheDocument(); - }); - - // Initially group-A is control, so its row shows "control" in p-value and lift columns. - // The non-control row (group-B) shows computed values. - await waitFor(() => { - expect(screen.getByText('33.3333')).toBeInTheDocument(); - }); + // Initially group-A is control, group-B shows the computed lift + const groupB = await screen.findByText('group-B'); + expect(await screen.findByText('33.3333')).toBeInTheDocument(); - // Click the group-B row to make it the new control. - // The row containing "group-B" text is what we need to click. - const groupBCell = screen.getByText('group-B'); - const groupBRow = groupBCell.closest('tr'); - expect(groupBRow).not.toBeNull(); - fireEvent.click(groupBRow!); + // Click the group-B row to make it the new control + fireEvent.click(groupB.closest('tr')!); - // After clicking, group-B becomes control. - // group-A lift: ((30 - 40) / 40) * 100 = -25.0000 - await waitFor(() => { - expect(screen.getByText('-25.0000')).toBeInTheDocument(); - }); + // group-A lift vs group-B control: ((30 - 40) / 40) * 100 = -25.0000 + expect(await screen.findByText('-25.0000')).toBeInTheDocument(); }); test('renders group name data in the table cells', async () => { render(); - await waitFor(() => { - expect(screen.getByText('group-A')).toBeInTheDocument(); - expect(screen.getByText('group-B')).toBeInTheDocument(); - }); + expect(await screen.findByText('group-A')).toBeInTheDocument(); + expect(screen.getByText('group-B')).toBeInTheDocument(); }); test('renders with three data rows and computes values for each non-control row', async () => { @@ -341,17 +284,9 @@ test('renders with three data rows and computes values for each non-control row' render(); - await waitFor(() => { - expect(screen.getByText('control-group')).toBeInTheDocument(); - expect(screen.getByText('test-group-1')).toBeInTheDocument(); - expect(screen.getByText('test-group-2')).toBeInTheDocument(); - }); - + await waitFor(() => expect(dataRows()).toHaveLength(3)); // control-group: sum = 20 - // test-group-1: sum = 30, lift = ((30-20)/20)*100 = 50.0000 - // test-group-2: sum = 40, lift = ((40-20)/20)*100 = 100.0000 - await waitFor(() => { - expect(screen.getByText('50.0000')).toBeInTheDocument(); - expect(screen.getByText('100.0000')).toBeInTheDocument(); - }); + // test-group-1: sum = 30, lift = 50.0000; test-group-2: sum = 40, lift = 100.0000 + expect(screen.getByText('50.0000')).toBeInTheDocument(); + expect(screen.getByText('100.0000')).toBeInTheDocument(); }); From d7c34215a231edb40e310cccd21f365fa6c9c30e Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 17 Jul 2026 20:36:09 -0700 Subject: [PATCH 45/84] fix(paired-t-test): bound the scroll container to the chart height The container relied on height: 100%, which does not resolve because the chart's parent is not height-bounded in the v1 render path, so tall output (many groups across multiple metrics) still clipped without scrolling. Thread the explicit height SuperChart passes via chartProps through transformProps and apply it to the scroll container, matching the pattern used by the pivot-table chart. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugin-chart-paired-t-test/src/PairedTTest.tsx | 9 +++++---- .../plugin-chart-paired-t-test/src/transformProps.ts | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx index 391e7f834840..dda51913416f 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/src/PairedTTest.tsx @@ -26,6 +26,7 @@ interface PairedTTestProps { className?: string; data: Record; groups: string[]; + height?: number; liftValPrec?: number; metrics: string[]; pValPrec?: number; @@ -33,9 +34,8 @@ interface PairedTTestProps { const StyledDiv = styled.div` ${({ theme }) => ` - /* Fill the chart's allotted height and scroll when the tables overflow it - (e.g. many groups across multiple metrics). */ - height: 100%; + /* The chart's allotted height is applied inline; scroll when the tables + overflow it (e.g. many groups across multiple metrics). */ overflow: auto; padding: 0 ${theme.sizeUnit}px; @@ -60,12 +60,13 @@ function PairedTTest({ className = '', data, groups, + height, liftValPrec = 4, metrics, pValPrec = 6, }: PairedTTestProps) { return ( - + {metrics.map((metric, i) => ( Date: Fri, 17 Jul 2026 20:36:09 -0700 Subject: [PATCH 46/84] chore(paired-t-test): drop the unused prop-types dependency Nothing in the plugin imports prop-types since the class components were migrated to function components. Remove it and its lockfile reference. Co-Authored-By: Claude Opus 4.8 (1M context) --- superset-frontend/package-lock.json | 3 --- .../plugins/plugin-chart-paired-t-test/package.json | 3 --- 2 files changed, 6 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index c09aab93727b..a28dd17842e9 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -44440,9 +44440,6 @@ "name": "@superset-ui/plugin-chart-paired-t-test", "version": "0.20.3", "license": "Apache-2.0", - "dependencies": { - "prop-types": "^15.8.1" - }, "peerDependencies": { "@apache-superset/core": "*", "@superset-ui/chart-controls": "*", diff --git a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json index 5810f252e5a0..59f4ec20d393 100644 --- a/superset-frontend/plugins/plugin-chart-paired-t-test/package.json +++ b/superset-frontend/plugins/plugin-chart-paired-t-test/package.json @@ -22,9 +22,6 @@ "esm", "lib" ], - "dependencies": { - "prop-types": "^15.8.1" - }, "peerDependencies": { "@superset-ui/chart-controls": "*", "@superset-ui/core": "*", From f2055d8446c5b9515c9e58c4d84e937528c31b55 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 10:25:09 -0700 Subject: [PATCH 47/84] fix(deck-multi): autozoom to layer data in the v1 fetch path deck.gl Multiple Layers computed its viewport once, from props.payload.data.features, and never again. In the v1 path that payload is empty (each layer is fetched client-side after mount), so autozoom had no points to fit and the camera stayed at the default location while the layers rendered off-screen -- the map showed but appeared to have no data. Accumulate each layer's features as it is fetched and refit the viewport to the combined points (when autozoom is enabled), mirroring what the legacy pre-merged payload allowed. Also factor the per-viz_type point collection into a shared helper. Adds a v1-path test covering both layer rendering and the viewport refit, which the existing suite (all legacy payload.data.slices) did not exercise. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../preset-chart-deckgl/src/Multi/Multi.tsx | 71 +++++++-- .../src/Multi/MultiV1.test.tsx | 144 ++++++++++++++++++ 2 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index a0db73367b73..ffd7a209d61d 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -112,6 +112,35 @@ const COLOR_AWARE_LAYER_DEFAULTS: Record = { deck_arc: COLOR_SCHEME_TYPES.fixed_color, }; +// Maps each layer viz_type to the helper that extracts its lat/lng points, so +// the viewport can be fitted to the combined data. In the v1 path the features +// are not pre-merged into one payload, so points are collected per layer as +// each one is fetched. +const GET_POINTS_BY_VIZ_TYPE: Record< + string, + (features: JsonObject[]) => [number, number][] +> = { + deck_polygon: getPointsPolygon, + deck_path: getPointsPath, + deck_grid: getPointsGrid, + deck_scatter: getPointsScatter, + deck_contour: getPointsContour, + deck_heatmap: getPointsHeatmap, + deck_hex: getPointsHex, + deck_arc: getPointsArc, + deck_geojson: getPointsGeojson, + deck_screengrid: getPointsScreengrid, +}; + +// Collect every layer's points from a { [vizType]: features[] } map. +const collectPoints = ( + featuresByVizType: Record, +): [number, number][] => + Object.entries(featuresByVizType).flatMap(([vizType, features]) => { + const getPoints = GET_POINTS_BY_VIZ_TYPE[vizType]; + return getPoints ? getPoints(features || []) : []; + }); + const selectDataMask = createSelector( (state: { dataMask?: DataMaskState }) => state.dataMask, dataMask => dataMask || {}, @@ -138,18 +167,7 @@ const DeckMulti = (props: DeckMultiProps) => { // fetches every layer client-side, so there may be none here const features = props.payload?.data?.features || {}; if (props.formData.autozoom !== false) { - const points = [ - ...getPointsPolygon(features.deck_polygon || []), - ...getPointsPath(features.deck_path || []), - ...getPointsGrid(features.deck_grid || []), - ...getPointsScatter(features.deck_scatter || []), - ...getPointsContour(features.deck_contour || []), - ...getPointsHeatmap(features.deck_heatmap || []), - ...getPointsHex(features.deck_hex || []), - ...getPointsArc(features.deck_arc || []), - ...getPointsGeojson(features.deck_geojson || []), - ...getPointsScreengrid(features.deck_screengrid || []), - ]; + const points = collectPoints(features); if (props.formData && points.length > 0) { viewport = fitViewport(viewport, { @@ -171,6 +189,12 @@ const DeckMulti = (props: DeckMultiProps) => { {}, ); const [layerOrder, setLayerOrder] = useState([]); + // Accumulates each layer's fetched features (keyed by viz_type) so autozoom + // can fit the viewport to the combined data. In the v1 path the features are + // fetched per layer rather than pre-merged into props.payload, so the initial + // getAdjustedViewport has nothing to fit to and the viewport is recomputed + // here as each layer arrives. + const layerFeaturesRef = useRef>({}); const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => { const { current } = containerRef; @@ -379,6 +403,26 @@ const DeckMulti = (props: DeckMultiProps) => { ...subSlicesLayers, [subsliceCopy.slice_id]: layer, })); + + // Refit the viewport to the data now that this layer's features are + // known (unless autozoom is off). The initial getAdjustedViewport + // could not do this because the v1 payload carries no features. + const layerFeatures = (layerProps.payload as JsonObject | undefined) + ?.data?.features; + if (formData.autozoom !== false && Array.isArray(layerFeatures)) { + layerFeaturesRef.current = { + ...layerFeaturesRef.current, + [vizType]: layerFeatures, + }; + const points = collectPoints(layerFeaturesRef.current); + if (points.length > 0) { + const fitted = fitViewport( + { ...props.viewport }, + { width: props.width, height: props.height, points }, + ); + setViewport(fitted.zoom < 0 ? { ...fitted, zoom: 0 } : fitted); + } + } }); }) .catch(error => { @@ -394,6 +438,7 @@ const DeckMulti = (props: DeckMultiProps) => { props.width, props.height, props.datasource, + props.viewport, theme, ], ); @@ -406,6 +451,8 @@ const DeckMulti = (props: DeckMultiProps) => { ): void => { setViewport(getAdjustedViewport()); setSubSlicesLayers({}); + // Start a fresh feature accumulation for the incremental autozoom refit. + layerFeaturesRef.current = {}; let visibleDeckLayers = visibleLayers; diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx new file mode 100644 index 000000000000..cf0ca45604b2 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx @@ -0,0 +1,144 @@ +/** + * 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 { configureStore } from '@reduxjs/toolkit'; +import { Provider } from 'react-redux'; +import { render, screen, waitFor } from '@testing-library/react'; +import { ThemeProvider, supersetTheme } from '@apache-superset/core/theme'; +import { DatasourceType, SupersetClient } from '@superset-ui/core'; +import DeckMulti from './Multi'; + +jest.mock('../DeckGLContainer', () => ({ + DeckGLContainerStyledWrapper: ({ viewport, layers }: any) => ( +
+ DeckGL Container Mock +
+ ), +})); + +jest.mock('@superset-ui/core', () => ({ + ...jest.requireActual('@superset-ui/core'), + SupersetClient: { get: jest.fn(), post: jest.fn() }, +})); + +// San Francisco coordinates so the fitted viewport is far from the default (0,0). +const SF_FEATURES = [ + { position: [-122.4, 37.8] }, + { position: [-122.42, 37.77] }, +]; + +const { getChartBuildQueryRegistry, getChartTransformPropsRegistry } = + jest.requireActual('@superset-ui/core'); +getChartBuildQueryRegistry().registerValue('deck_scatter', (fd: any) => ({ + datasource: 'test_datasource', + queries: [{}], + form_data: fd, +})); +getChartTransformPropsRegistry().registerValue('deck_scatter', () => ({ + payload: { data: { features: SF_FEATURES } }, +})); + +const store = configureStore({ reducer: { dataMask: () => ({}) } }); + +// The v1 path: deck_multi issues no query of its own, so payload (queriesData[0]) +// is undefined and each layer is fetched from its saved chart client-side. +const v1Props: any = { + formData: { + datasource: 'test_datasource', + viz_type: 'deck_multi', + deck_slices: [1], + autozoom: true, + map_style: 'mapbox://styles/mapbox/light-v9', + }, + payload: undefined, + setControlValue: jest.fn(), + viewport: { longitude: 0, latitude: 0, zoom: 1 }, + onAddFilter: jest.fn(), + height: 600, + width: 800, + datasource: { + id: 1, + type: DatasourceType.Table, + name: 'test_datasource', + columns: [], + metrics: [], + columnFormats: {}, + currencyFormats: {}, + verboseMap: {}, + }, + onSelect: jest.fn(), +}; + +const renderV1 = () => + render( + + + + + , + ); + +beforeEach(() => { + jest.clearAllMocks(); + (SupersetClient.get as jest.Mock).mockResolvedValue({ + json: { + result: { + viz_type: 'deck_scatter', + params: JSON.stringify({ + viz_type: 'deck_scatter', + datasource: 'test_datasource', + }), + }, + }, + }); + (SupersetClient.post as jest.Mock).mockResolvedValue({ + json: { result: [{ data: SF_FEATURES }] }, + }); +}); + +test('fetches the saved sub-slice and renders its layer (v1 path)', async () => { + renderV1(); + + await waitFor(() => expect(SupersetClient.get).toHaveBeenCalled()); + await waitFor(() => expect(SupersetClient.post).toHaveBeenCalled()); + await waitFor(() => + expect( + screen.getByTestId('deckgl-container').getAttribute('data-layers-count'), + ).toBe('1'), + ); +}); + +test('fits the viewport to the fetched layer data when autozoom is on (v1 path)', async () => { + renderV1(); + + // The initial viewport has no data to fit to (payload is empty in v1), so it + // starts at the default. Once the layer's features arrive it should recenter + // near the data (San Francisco), not stay at (0, 0). + await waitFor(() => { + const viewport = JSON.parse( + screen.getByTestId('deckgl-container').getAttribute('data-viewport') || + '{}', + ); + expect(viewport.longitude).toBeLessThan(-100); + expect(viewport.latitude).toBeGreaterThan(30); + }); +}); From 9c6b1f0eb6b14edb6b09338012f278db6eae0f7f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 10:55:47 -0700 Subject: [PATCH 48/84] fix(deck-multi): correct the points-collection typing to unblock tsc The GET_POINTS_BY_VIZ_TYPE map annotated its values as (features: JsonObject[]) => [number, number][], but the per-layer getPoints helpers have looser, inconsistent signatures (getPointsGeojson takes Point[]), so the map failed tsc. Replace it with an explicit-spread collectPoints(features) mirroring the original getAdjustedViewport, which type-checks and works for both the payload and the per-layer accumulator. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../preset-chart-deckgl/src/Multi/Multi.tsx | 46 +++++++------------ 1 file changed, 17 insertions(+), 29 deletions(-) diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index ffd7a209d61d..743dbdeb81d4 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -112,34 +112,22 @@ const COLOR_AWARE_LAYER_DEFAULTS: Record = { deck_arc: COLOR_SCHEME_TYPES.fixed_color, }; -// Maps each layer viz_type to the helper that extracts its lat/lng points, so -// the viewport can be fitted to the combined data. In the v1 path the features -// are not pre-merged into one payload, so points are collected per layer as -// each one is fetched. -const GET_POINTS_BY_VIZ_TYPE: Record< - string, - (features: JsonObject[]) => [number, number][] -> = { - deck_polygon: getPointsPolygon, - deck_path: getPointsPath, - deck_grid: getPointsGrid, - deck_scatter: getPointsScatter, - deck_contour: getPointsContour, - deck_heatmap: getPointsHeatmap, - deck_hex: getPointsHex, - deck_arc: getPointsArc, - deck_geojson: getPointsGeojson, - deck_screengrid: getPointsScreengrid, -}; - -// Collect every layer's points from a { [vizType]: features[] } map. -const collectPoints = ( - featuresByVizType: Record, -): [number, number][] => - Object.entries(featuresByVizType).flatMap(([vizType, features]) => { - const getPoints = GET_POINTS_BY_VIZ_TYPE[vizType]; - return getPoints ? getPoints(features || []) : []; - }); +// Collect every layer's lat/lng points from a features map keyed by viz_type, +// so the viewport can be fitted to the combined data. In the v1 path the +// features are not pre-merged into one payload, so they are accumulated per +// layer as each one is fetched and passed here in the same shape. +const collectPoints = (features: JsonObject) => [ + ...getPointsPolygon(features.deck_polygon || []), + ...getPointsPath(features.deck_path || []), + ...getPointsGrid(features.deck_grid || []), + ...getPointsScatter(features.deck_scatter || []), + ...getPointsContour(features.deck_contour || []), + ...getPointsHeatmap(features.deck_heatmap || []), + ...getPointsHex(features.deck_hex || []), + ...getPointsArc(features.deck_arc || []), + ...getPointsGeojson(features.deck_geojson || []), + ...getPointsScreengrid(features.deck_screengrid || []), +]; const selectDataMask = createSelector( (state: { dataMask?: DataMaskState }) => state.dataMask, @@ -194,7 +182,7 @@ const DeckMulti = (props: DeckMultiProps) => { // fetched per layer rather than pre-merged into props.payload, so the initial // getAdjustedViewport has nothing to fit to and the viewport is recomputed // here as each layer arrives. - const layerFeaturesRef = useRef>({}); + const layerFeaturesRef = useRef({}); const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => { const { current } = containerRef; From f2fd5368237331ca77aae4c61813c54ed01d6ea1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 13:06:43 -0700 Subject: [PATCH 49/84] fix(deck-multi): query layers against their real datasource, not stale params Each deck.gl Multiple Layers sub-layer is an independent saved chart with its own dataset, and its layer query must hit that dataset. fetchSubslices rebuilt the layer form_data from the saved params, whose `datasource` string can be stale -- example charts hardcode a datasource id that does not match the imported dataset's real id. The layer query then went to the wrong dataset and the server rejected it with 400 "Columns missing in dataset" (e.g. LON/LAT), so no layer rendered. Standalone the chart works because it resolves its datasource from the slice relationship, not params. Use the chart's authoritative datasource_id/datasource_type from the chart API to build the layer datasource, falling back to params only when absent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../preset-chart-deckgl/src/Multi/Multi.tsx | 10 +++++++++ .../src/Multi/MultiV1.test.tsx | 21 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index 743dbdeb81d4..e37dc57f8314 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -498,10 +498,20 @@ const DeckMulti = (props: DeckMultiProps) => { } catch { params = {}; } + // The saved params carry a `datasource` string, but it can be + // stale (e.g. example charts hardcode an id that differs from the + // imported dataset's real id). Prefer the chart's authoritative + // datasource_id/datasource_type so the layer queries the dataset + // it is actually bound to, the same one it uses standalone. + const datasource = + result.datasource_id != null && result.datasource_type + ? `${result.datasource_id}__${result.datasource_type}` + : params.datasource; return { slice_id: sliceId, form_data: { ...params, + datasource, slice_id: sliceId, viz_type: result.viz_type ?? params.viz_type, }, diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx index cf0ca45604b2..4130d2755cf0 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/MultiV1.test.tsx @@ -49,7 +49,7 @@ const SF_FEATURES = [ const { getChartBuildQueryRegistry, getChartTransformPropsRegistry } = jest.requireActual('@superset-ui/core'); getChartBuildQueryRegistry().registerValue('deck_scatter', (fd: any) => ({ - datasource: 'test_datasource', + datasource: fd.datasource, queries: [{}], form_data: fd, })); @@ -103,9 +103,13 @@ beforeEach(() => { json: { result: { viz_type: 'deck_scatter', + // The chart's authoritative datasource, which differs from the stale + // value baked into params below. + datasource_id: 42, + datasource_type: 'table', params: JSON.stringify({ viz_type: 'deck_scatter', - datasource: 'test_datasource', + datasource: '5__table', }), }, }, @@ -127,6 +131,19 @@ test('fetches the saved sub-slice and renders its layer (v1 path)', async () => ); }); +test('queries the layer against the chart real datasource, not the stale params one', async () => { + renderV1(); + + // The saved params say "5__table" but the chart is bound to + // datasource_id 42; the layer query must use the authoritative "42__table" + // so it hits the dataset that actually has the layer's columns. + await waitFor(() => expect(SupersetClient.post).toHaveBeenCalled()); + const payload = (SupersetClient.post as jest.Mock).mock.calls[0][0] + .jsonPayload; + expect(payload.form_data.datasource).toBe('42__table'); + expect(payload.datasource).toBe('42__table'); +}); + test('fits the viewport to the fetched layer data when autozoom is on (v1 path)', async () => { renderV1(); From 09a034c573e25e197d2fa623e14d4caebc2ea137 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 14:56:16 -0700 Subject: [PATCH 50/84] test(plugins): cover the migrated calendar/chord/country-map registration Codecov flagged the calendar transformProps and the calendar/chord/ country-map plugin index modules as uncovered. Add plugin-registration tests (instantiate the plugin, assert v1 metadata and the buildQuery loader), a calendar transformProps test (v1 reshape plus display-option passthrough, and the non-array passthrough branch), and a calendar buildQuery case for an unrecognized subdomain granularity falling back to minutes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../test/buildQuery.test.ts | 8 ++ .../plugin-chart-calendar/test/index.test.ts | 27 ++++++ .../test/transformProps.test.ts | 88 +++++++++++++++++++ .../plugin-chart-chord/test/index.test.ts | 27 ++++++ .../test/index.test.ts | 27 ++++++ 5 files changed, 177 insertions(+) create mode 100644 superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-calendar/test/transformProps.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-chord/test/index.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts diff --git a/superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts index 54a95eb939c1..120e1ab07e25 100644 --- a/superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-calendar/test/buildQuery.test.ts @@ -43,3 +43,11 @@ test('defaults the time grain to minutes like the legacy backend', () => { }).queries; expect(query.extras?.time_grain_sqla).toEqual('PT1M'); }); + +test('falls back to minutes for an unrecognized subdomain granularity', () => { + const [query] = buildQuery({ + ...formData, + subdomain_granularity: 'fortnight', + }).queries; + expect(query.extras?.time_grain_sqla).toEqual('PT1M'); +}); diff --git a/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts new file mode 100644 index 000000000000..8e1d45f1ac64 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts @@ -0,0 +1,27 @@ +/** + * 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 CalendarChartPlugin from '../src/index'; + +test('registers the plugin with its v1 metadata and buildQuery loader', () => { + const plugin = new CalendarChartPlugin(); + expect(plugin.metadata.name).toEqual('Calendar Heatmap'); + // Migrated off the legacy explore_json pipeline, so it must not opt back in. + expect(plugin.metadata.useLegacyApi).toBeFalsy(); + expect(plugin.loadBuildQuery).toBeDefined(); +}); diff --git a/superset-frontend/plugins/plugin-chart-calendar/test/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/transformProps.test.ts new file mode 100644 index 000000000000..21283837e754 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-calendar/test/transformProps.test.ts @@ -0,0 +1,88 @@ +/** + * 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 { supersetTheme } from '@apache-superset/core/theme'; +import { ChartProps } from '@superset-ui/core'; +import transformProps from '../src/transformProps'; + +const baseChartProps = { + width: 800, + height: 600, + datasource: { verboseMap: { SUM__value: 'Sum value' } }, + theme: supersetTheme, + hooks: {}, +}; + +const formData = { + metrics: ['SUM__value'], + domainGranularity: 'month', + subdomainGranularity: 'day', + linearColorScheme: 'schemeRdYlBu', + cellSize: 10, + cellPadding: 2, + cellRadius: 0, + steps: 10, + yAxisFormat: '.2f', + xAxisTimeFormat: '%b %d', + showLegend: true, + showValues: false, + showMetricName: true, + sliceId: 1, +}; + +test('reshapes v1 records into calendar data and passes through display options', () => { + const chartProps = new ChartProps({ + ...baseChartProps, + formData, + queriesData: [ + { + data: [ + { __timestamp: Date.UTC(2020, 0, 1), SUM__value: 3 }, + { __timestamp: Date.UTC(2020, 0, 2), SUM__value: 7 }, + ], + from_dttm: Date.UTC(2020, 0, 1), + to_dttm: Date.UTC(2020, 0, 31), + }, + ], + }); + + const result = transformProps(chartProps) as Record; + + // The reshape produces a per-metric value map, and the display controls and + // formatters are forwarded to the chart. + expect(result.data).toBeDefined(); + expect(result.domainGranularity).toBe('month'); + expect(result.subdomainGranularity).toBe('day'); + expect(result.cellSize).toBe(10); + expect(result.showMetricName).toBe(true); + expect(typeof result.timeFormatter).toBe('function'); + expect(typeof result.valueFormatter).toBe('function'); + expect(result.verboseMap).toEqual({ SUM__value: 'Sum value' }); +}); + +test('passes through non-array data untouched (already-shaped payload)', () => { + const preShaped = { SUM__value: { 1577836800000: 3 } }; + const chartProps = new ChartProps({ + ...baseChartProps, + formData, + queriesData: [{ data: preShaped, from_dttm: null, to_dttm: null }], + }); + + const result = transformProps(chartProps) as Record; + expect(result.data).toBe(preShaped); +}); diff --git a/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts b/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts new file mode 100644 index 000000000000..4a01f0ed37a0 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts @@ -0,0 +1,27 @@ +/** + * 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 ChordChartPlugin from '../src/index'; + +test('registers the plugin with its v1 metadata and buildQuery loader', () => { + const plugin = new ChordChartPlugin(); + expect(plugin.metadata.name).toEqual('Chord Diagram'); + // Migrated off the legacy explore_json pipeline, so it must not opt back in. + expect(plugin.metadata.useLegacyApi).toBeFalsy(); + expect(plugin.loadBuildQuery).toBeDefined(); +}); diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts new file mode 100644 index 000000000000..d9e107eceb84 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts @@ -0,0 +1,27 @@ +/** + * 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 CountryMapChartPlugin from '../src/index'; + +test('registers the plugin with its v1 metadata and buildQuery loader', () => { + const plugin = new CountryMapChartPlugin(); + expect(plugin.metadata.name).toEqual('Country Map'); + // Migrated off the legacy explore_json pipeline, so it must not opt back in. + expect(plugin.metadata.useLegacyApi).toBeFalsy(); + expect(plugin.loadBuildQuery).toBeDefined(); +}); From b2d2aa4452628c70f8f3b72736694adab961788f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 15:05:13 -0700 Subject: [PATCH 51/84] feat(deck-multi): surface layer load failures in the chart When a deck.gl Multiple Layers sub-layer fails to load (e.g. it is bound to a dataset that lacks the columns it needs), the error previously only threw to the browser console, leaving the map looking silently empty. Capture the per-slice failure and render a warning Alert over the map, normalized via getClientErrorObject so the server message (e.g. "Columns missing in dataset") is shown. Errors reset on each reload. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../preset-chart-deckgl/src/Multi/Multi.tsx | 40 +++++++++++++++++-- .../src/Multi/MultiV1.test.tsx | 20 ++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx index e37dc57f8314..b5033e130801 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/Multi/Multi.tsx @@ -34,6 +34,7 @@ import { FilterState, getChartBuildQueryRegistry, getChartTransformPropsRegistry, + getClientErrorObject, HandlerFunction, isDefined, JsonObject, @@ -44,7 +45,9 @@ import { getMapProviderMapStyle, usePrevious, } from '@superset-ui/core'; +import { t } from '@apache-superset/core/translation'; import { styled, useTheme } from '@apache-superset/core/theme'; +import { Alert } from '@apache-superset/core/components'; import { Layer } from '@deck.gl/core'; import { @@ -177,6 +180,9 @@ const DeckMulti = (props: DeckMultiProps) => { {}, ); const [layerOrder, setLayerOrder] = useState([]); + // Per-slice error messages for layers that failed to load, so the failure is + // surfaced in the chart instead of only the browser console. + const [layerErrors, setLayerErrors] = useState>({}); // Accumulates each layer's fetched features (keyed by viz_type) so autozoom // can fit the viewport to the combined data. In the v1 path the features are // fetched per layer rather than pre-merged into props.payload, so the initial @@ -413,10 +419,17 @@ const DeckMulti = (props: DeckMultiProps) => { } }); }) - .catch(error => { - throw new Error( - `Error loading layer for slice ${subsliceCopy.slice_id}: ${error}`, - ); + .catch(async error => { + // Surface the failure in the chart (e.g. a layer bound to a dataset + // that is missing the columns it needs) rather than only throwing to + // the console. + const { message, error: errorText } = + await getClientErrorObject(error); + setLayerErrors(layerErrors => ({ + ...layerErrors, + [subsliceCopy.slice_id]: + message || errorText || 'Failed to load layer', + })); }); }, [ @@ -439,6 +452,7 @@ const DeckMulti = (props: DeckMultiProps) => { ): void => { setViewport(getAdjustedViewport()); setSubSlicesLayers({}); + setLayerErrors({}); // Start a fresh feature accumulation for the incremental autozoom refit. layerFeaturesRef.current = {}; @@ -572,8 +586,26 @@ const DeckMulti = (props: DeckMultiProps) => { legacyMapStyle: formData.map_style, }); + const errorMessages = Object.values(layerErrors); + return ( + {errorMessages.length > 0 && ( + + )} { + // e.g. a layer bound to a dataset missing its columns: the server rejects + // the layer query and the failure should be visible, not console-only. + (SupersetClient.post as jest.Mock).mockRejectedValue( + new Response( + JSON.stringify({ message: "Columns missing in dataset: ['LON', 'LAT']" }), + { status: 400 }, + ), + ); + + renderV1(); + + expect( + await screen.findByText('Some layers could not be loaded'), + ).not.toBeNull(); + expect(screen.getByText(/Columns missing in dataset/)).not.toBeNull(); + // The map still renders alongside the warning. + expect(screen.getByTestId('deckgl-container')).not.toBeNull(); +}); From 6edeafff6bab4bcfd1710028445d7d962ba3782b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 18 Jul 2026 16:56:30 -0700 Subject: [PATCH 52/84] fix(deck-arc): default missing layer colors instead of crashing The Arc layer read color_picker/target_color_picker straight off form data and dereferenced .r/.g/.b, so a saved chart without those controls crashed the ArcLayer on init ("Cannot read properties of undefined (reading 'r')"). This surfaced in deck.gl Multiple Layers, where sub-slices arrive as raw saved form data without control-default hydration and the Arc example never persisted target_color_picker. Fall back to the control-panel default color (PRIMARY_COLOR) when a picker is absent, matching how Path and Scatter already guard, and harden Polygon's fill/stroke pickers the same way. Adds Arc color-accessor tests for the missing and present cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/layers/Arc/Arc.test.ts | 63 +++++++++++++++++++ .../src/layers/Arc/Arc.tsx | 10 ++- .../src/layers/Polygon/Polygon.tsx | 7 ++- 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts new file mode 100644 index 000000000000..8d1a30ff9ed6 --- /dev/null +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts @@ -0,0 +1,63 @@ +/** + * 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 { COLOR_SCHEME_TYPES } from '../../utilities/utils'; +import { getLayer } from './Arc'; + +const baseArgs = { + payload: { data: { features: [] } }, + setTooltip: () => {}, + setDataMask: () => {}, + onContextMenu: () => {}, + filterState: {}, + datasource: {}, + onSelect: () => {}, + emitCrossFilters: false, +} as any; + +const sampleDatum = {} as any; + +test('falls back to the default color when target_color_picker is missing', () => { + // Sub-slices in deck.gl Multiple Layers arrive without control-default + // hydration, and the Arc example never saved target_color_picker; the color + // accessors must not crash on the missing color. + const formData = { + color_scheme_type: COLOR_SCHEME_TYPES.fixed_color, + color_picker: { r: 10, g: 20, b: 30, a: 1 }, + // target_color_picker intentionally omitted + } as any; + + const layer = getLayer({ ...baseArgs, formData }); + + expect(layer.props.getSourceColor(sampleDatum)).toEqual([10, 20, 30, 255]); + // Missing target color resolves to PRIMARY_COLOR { r:0, g:122, b:135, a:1 } + expect(layer.props.getTargetColor(sampleDatum)).toEqual([0, 122, 135, 255]); +}); + +test('uses the configured source and target colors when both are present', () => { + const formData = { + color_scheme_type: COLOR_SCHEME_TYPES.fixed_color, + color_picker: { r: 10, g: 20, b: 30, a: 1 }, + target_color_picker: { r: 40, g: 50, b: 60, a: 1 }, + } as any; + + const layer = getLayer({ ...baseArgs, formData }); + + expect(layer.props.getSourceColor(sampleDatum)).toEqual([10, 20, 30, 255]); + expect(layer.props.getTargetColor(sampleDatum)).toEqual([40, 50, 60, 255]); +}); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.tsx index f8a6f84a9255..3b80cc0f375b 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.tsx @@ -19,6 +19,7 @@ import { ArcLayer } from '@deck.gl/layers'; import { JsonObject, QueryFormData } from '@superset-ui/core'; import { COLOR_SCHEME_TYPES } from '../../utilities/utils'; +import { PRIMARY_COLOR } from '../../utilities/controls'; import { commonLayerProps } from '../common'; import { GetLayerType, createCategoricalDeckGLComponent } from '../../factory'; import { Point } from '../../types'; @@ -69,8 +70,13 @@ export const getLayer: GetLayerType = function ({ }) { const fd = formData; const data = payload.data.features; - const sc = fd.color_picker; - const tc = fd.target_color_picker; + // Fall back to the control-panel defaults when the color pickers are absent. + // Sub-slices rendered inside deck.gl Multiple Layers arrive as raw saved form + // data without control-default hydration, and some saved charts (e.g. the Arc + // example) never persisted target_color_picker, which would otherwise crash + // the color accessors on `undefined`. + const sc = fd.color_picker ?? PRIMARY_COLOR; + const tc = fd.target_color_picker ?? PRIMARY_COLOR; const colorSchemeType = fd.color_scheme_type; diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.tsx index e7caa2110cc4..96635a158ae6 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Polygon/Polygon.tsx @@ -47,6 +47,7 @@ import { } from '../../utils'; import { commonLayerProps, getColorForBreakpoints } from '../common'; +import { PRIMARY_COLOR } from '../../utilities/controls'; import getPointsFromPolygon from '../../utils/getPointsFromPolygon'; import fitViewport, { Viewport } from '../../utils/fitViewport'; import { @@ -115,10 +116,12 @@ export const getLayer: GetLayerType = function ({ emitCrossFilters, }) { const fd = formData as PolygonFormData; + // Default the color pickers when absent so unhydrated sub-slice form data + // (as passed by deck.gl Multiple Layers) does not crash the color scalers. const fc: { r: number; g: number; b: number; a: number } = - fd.fill_color_picker; + fd.fill_color_picker ?? PRIMARY_COLOR; const sc: { r: number; g: number; b: number; a: number } = - fd.stroke_color_picker; + fd.stroke_color_picker ?? PRIMARY_COLOR; const defaultBreakpointColor = fd.default_breakpoint_color; const data = [...payload.data.features]; From 2be95c6283b8bb44f1f22f7150a8c902ad95a4d1 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 18 Jul 2026 21:25:24 -0700 Subject: [PATCH 53/84] fix: repair type errors from removed useLegacyApi metadata field Three plugin tests still asserted on ChartMetadata.useLegacyApi, which this PR drops entirely, and the Arc layer test called deck.gl color accessors without narrowing their Accessor union type. Both broke lint-frontend's tsc check. Co-Authored-By: Claude Opus 4.8 --- .../plugin-chart-calendar/test/index.test.ts | 2 -- .../plugin-chart-chord/test/index.test.ts | 2 -- .../test/index.test.ts | 2 -- .../src/layers/Arc/Arc.test.ts | 21 +++++++++++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts b/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts index 8e1d45f1ac64..ebd4d3c14ef7 100644 --- a/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts +++ b/superset-frontend/plugins/plugin-chart-calendar/test/index.test.ts @@ -21,7 +21,5 @@ import CalendarChartPlugin from '../src/index'; test('registers the plugin with its v1 metadata and buildQuery loader', () => { const plugin = new CalendarChartPlugin(); expect(plugin.metadata.name).toEqual('Calendar Heatmap'); - // Migrated off the legacy explore_json pipeline, so it must not opt back in. - expect(plugin.metadata.useLegacyApi).toBeFalsy(); expect(plugin.loadBuildQuery).toBeDefined(); }); diff --git a/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts b/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts index 4a01f0ed37a0..3bdcfa1cfbec 100644 --- a/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts +++ b/superset-frontend/plugins/plugin-chart-chord/test/index.test.ts @@ -21,7 +21,5 @@ import ChordChartPlugin from '../src/index'; test('registers the plugin with its v1 metadata and buildQuery loader', () => { const plugin = new ChordChartPlugin(); expect(plugin.metadata.name).toEqual('Chord Diagram'); - // Migrated off the legacy explore_json pipeline, so it must not opt back in. - expect(plugin.metadata.useLegacyApi).toBeFalsy(); expect(plugin.loadBuildQuery).toBeDefined(); }); diff --git a/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts b/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts index d9e107eceb84..831056cb5f72 100644 --- a/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts +++ b/superset-frontend/plugins/plugin-chart-country-map/test/index.test.ts @@ -21,7 +21,5 @@ import CountryMapChartPlugin from '../src/index'; test('registers the plugin with its v1 metadata and buildQuery loader', () => { const plugin = new CountryMapChartPlugin(); expect(plugin.metadata.name).toEqual('Country Map'); - // Migrated off the legacy explore_json pipeline, so it must not opt back in. - expect(plugin.metadata.useLegacyApi).toBeFalsy(); expect(plugin.loadBuildQuery).toBeDefined(); }); diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts index 8d1a30ff9ed6..4188c4db4452 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts @@ -32,6 +32,11 @@ const baseArgs = { const sampleDatum = {} as any; +// The deck.gl Accessor type allows a plain color constant in addition to a +// per-datum function, so TS can't infer these are callable from the layer +// props alone; the test data always configures a function accessor. +type ColorAccessor = (datum: unknown) => number[]; + test('falls back to the default color when target_color_picker is missing', () => { // Sub-slices in deck.gl Multiple Layers arrive without control-default // hydration, and the Arc example never saved target_color_picker; the color @@ -44,9 +49,13 @@ test('falls back to the default color when target_color_picker is missing', () = const layer = getLayer({ ...baseArgs, formData }); - expect(layer.props.getSourceColor(sampleDatum)).toEqual([10, 20, 30, 255]); + expect((layer.props.getSourceColor as ColorAccessor)(sampleDatum)).toEqual([ + 10, 20, 30, 255, + ]); // Missing target color resolves to PRIMARY_COLOR { r:0, g:122, b:135, a:1 } - expect(layer.props.getTargetColor(sampleDatum)).toEqual([0, 122, 135, 255]); + expect((layer.props.getTargetColor as ColorAccessor)(sampleDatum)).toEqual([ + 0, 122, 135, 255, + ]); }); test('uses the configured source and target colors when both are present', () => { @@ -58,6 +67,10 @@ test('uses the configured source and target colors when both are present', () => const layer = getLayer({ ...baseArgs, formData }); - expect(layer.props.getSourceColor(sampleDatum)).toEqual([10, 20, 30, 255]); - expect(layer.props.getTargetColor(sampleDatum)).toEqual([40, 50, 60, 255]); + expect((layer.props.getSourceColor as ColorAccessor)(sampleDatum)).toEqual([ + 10, 20, 30, 255, + ]); + expect((layer.props.getTargetColor as ColorAccessor)(sampleDatum)).toEqual([ + 40, 50, 60, 255, + ]); }); From ca17830175edf3c143e8455bb2f32f37650bf17f Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 19 Jul 2026 01:02:21 -0700 Subject: [PATCH 54/84] fix: cast Arc test color accessors through unknown for tsc lint-frontend's tsc build failed with TS2352 on the direct Accessor -> ColorAccessor cast; go through unknown first as the compiler suggests. Co-Authored-By: Claude Opus 4.8 --- .../src/layers/Arc/Arc.test.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts index 4188c4db4452..ca369d48bcef 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts +++ b/superset-frontend/plugins/preset-chart-deckgl/src/layers/Arc/Arc.test.ts @@ -49,13 +49,13 @@ test('falls back to the default color when target_color_picker is missing', () = const layer = getLayer({ ...baseArgs, formData }); - expect((layer.props.getSourceColor as ColorAccessor)(sampleDatum)).toEqual([ - 10, 20, 30, 255, - ]); + expect( + (layer.props.getSourceColor as unknown as ColorAccessor)(sampleDatum), + ).toEqual([10, 20, 30, 255]); // Missing target color resolves to PRIMARY_COLOR { r:0, g:122, b:135, a:1 } - expect((layer.props.getTargetColor as ColorAccessor)(sampleDatum)).toEqual([ - 0, 122, 135, 255, - ]); + expect( + (layer.props.getTargetColor as unknown as ColorAccessor)(sampleDatum), + ).toEqual([0, 122, 135, 255]); }); test('uses the configured source and target colors when both are present', () => { @@ -67,10 +67,10 @@ test('uses the configured source and target colors when both are present', () => const layer = getLayer({ ...baseArgs, formData }); - expect((layer.props.getSourceColor as ColorAccessor)(sampleDatum)).toEqual([ - 10, 20, 30, 255, - ]); - expect((layer.props.getTargetColor as ColorAccessor)(sampleDatum)).toEqual([ - 40, 50, 60, 255, - ]); + expect( + (layer.props.getSourceColor as unknown as ColorAccessor)(sampleDatum), + ).toEqual([10, 20, 30, 255]); + expect( + (layer.props.getTargetColor as unknown as ColorAccessor)(sampleDatum), + ).toEqual([40, 50, 60, 255]); }); From 18231d9bd4f7661cc5aecdb1b194f09a042d5953 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 20 Jul 2026 17:09:54 -0700 Subject: [PATCH 55/84] feat(timeseries): percent-change rebasing with a draggable baseline (#42247) Co-authored-by: Claude Code --- .../src/Timeseries/EchartsTimeseries.tsx | 145 ++++++++++++++++++ .../Timeseries/Regular/Line/controlPanel.tsx | 16 ++ .../src/Timeseries/percentChange.ts | 113 ++++++++++++++ .../src/Timeseries/transformProps.ts | 17 +- .../test/Timeseries/percentChange.test.ts | 98 ++++++++++++ .../shared/migrate_viz/processors.py | 7 + .../viz/nvd3_compare_chart_to_echarts_test.py | 2 + 7 files changed, 396 insertions(+), 2 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/percentChange.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/percentChange.test.ts diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 7011128b03de..91fd4be02cfa 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -27,6 +27,7 @@ import { LegendState, ensureIsArray, } from '@superset-ui/core'; +import { useTheme } from '@apache-superset/core/theme'; import type { ECElementEvent, ViewRootGroup, @@ -35,6 +36,11 @@ import type GlobalModel from 'echarts/types/src/model/Global'; import type ComponentModel from 'echarts/types/src/model/Component'; import { EchartsHandler, EventHandlers } from '../types'; import Echart from '../components/Echart'; +import { + rebaseSeriesData, + snapToNearestX, + SeriesDataPoint, +} from './percentChange'; import { OrientationType, TimeseriesChartTransformedProps } from './types'; import { formatSeriesName } from '../utils/series'; import { ExtraControls } from '../components/ExtraControls'; @@ -63,10 +69,149 @@ export default function EchartsTimeseries({ onLegendScroll, }: TimeseriesChartTransformedProps) { const { stack } = formData; + const theme = useTheme(); const echartRef = useRef(null); // eslint-disable-next-line no-param-reassign refs.echartRef = echartRef; const clickTimer = useRef>(); + + // Draggable percent-change baseline: when the rebase view is active, a + // vertical line is drawn on the plot; dragging it re-indexes every series + // to the hovered point via the composable rebase, entirely client-side. + const rebaseEnabled = Boolean( + (formData as { rebasePercentChange?: boolean }).rebasePercentChange, + ); + // Persists the dragged baseline across effect reruns (e.g. resizes or + // other option changes) so those don't silently snap it back to the + // first point. + const baselineXRef = useRef(null); + useEffect(() => { + if (!rebaseEnabled) return undefined; + const chart = echartRef.current?.getEchartInstance?.(); + if (!chart) return undefined; + + const option = chart.getOption() as { + series?: { data?: SeriesDataPoint[] }[]; + }; + const baseSeries = (option.series ?? []).map(s => + Array.isArray(s.data) + ? (s.data.filter(Array.isArray) as SeriesDataPoint[]) + : [], + ); + // Preserve the axis' native x type: numeric for time/value axes, + // string for category axes (coercing categories with Number() would + // turn them into NaN and break snapping/positioning below). + const xs = Array.from(new Set(baseSeries.flat().map(([x]) => x))); + if (xs.length === 0) return undefined; + if (typeof xs[0] === 'number') { + (xs as number[]).sort((a, b) => a - b); + } + let baselineX = + baselineXRef.current !== null && xs.includes(baselineXRef.current) + ? baselineXRef.current + : xs[0]; + baselineXRef.current = baselineX; + // Coalesces drag updates to at most one setOption per animation + // frame; rebasing every series on every raw pointer-move event + // can stutter on large charts. + let dragFrame: ReturnType | null = null; + + const applyBaseline = (newX: number | string) => { + baselineX = newX; + baselineXRef.current = newX; + chart.setOption({ + series: baseSeries.map(data => ({ + data: rebaseSeriesData(data, newX), + })), + }); + }; + + const drawHandle = () => { + // Cap the handle to the plot area so it doesn't run through the + // legend above or the axis labels below. + let gridRect = { top: 0, height: chart.getHeight() }; + try { + const rect = ( + chart as unknown as { + getModel: () => { + getComponent: ( + type: string, + index: number, + ) => { + coordinateSystem: { + getRect: () => { y: number; height: number }; + }; + }; + }; + } + ) + .getModel() + .getComponent('grid', 0) + .coordinateSystem.getRect(); + gridRect = { top: rect.y, height: rect.height }; + } catch { + // fall back to the full chart height + } + let px: number; + try { + [px] = [chart.convertToPixel({ xAxisIndex: 0 }, baselineX) as number]; + } catch { + return; + } + chart.setOption({ + graphic: [ + { + id: 'percent-change-baseline', + // only group elements support children in the graphic API + type: 'group', + x: px - 4, + y: gridRect.top, + cursor: 'ew-resize', + draggable: true, + z: 100, + ondrag(this: { x: number; y: number }) { + this.y = gridRect.top; + const dataX = chart.convertFromPixel( + { xAxisIndex: 0 }, + this.x + 4, + ) as number | string; + if (dragFrame !== null) return; + dragFrame = requestAnimationFrame(() => { + dragFrame = null; + const snapped = snapToNearestX(xs, dataX); + if (snapped !== undefined && snapped !== baselineX) { + applyBaseline(snapped); + } + }); + }, + ondragend: () => drawHandle(), + children: [ + { + type: 'rect', + shape: { x: 0, y: 0, width: 8, height: gridRect.height }, + style: { fill: theme.colorFillSecondary }, + }, + { + type: 'rect', + shape: { x: 3, y: 0, width: 2, height: gridRect.height }, + style: { fill: theme.colorTextSecondary }, + }, + ], + }, + ], + }); + }; + drawHandle(); + + return () => { + if (dragFrame !== null) { + cancelAnimationFrame(dragFrame); + } + chart.setOption({ + graphic: [{ id: 'percent-change-baseline', $action: 'remove' }], + }); + }; + }, [rebaseEnabled, echartOptions, width, height, theme]); const extraControlRef = useRef(null); const [extraControlHeight, setExtraControlHeight] = useState(0); useEffect(() => { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index 63d7fff9a6d9..9b9019c4e23c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -106,6 +106,22 @@ const config: ControlPanelConfig = { }, }, ], + [ + { + name: 'rebase_percent_change', + config: { + type: 'CheckboxControl', + label: t('Percent change'), + renderTrigger: true, + default: false, + description: t( + 'Rebase each series to its percent change from a baseline ' + + 'point, like the legacy Time-series Percent Change chart. ' + + 'Drag the vertical baseline on the chart to re-index.', + ), + }, + }, + ], [ { name: 'markerEnabled', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/percentChange.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/percentChange.ts new file mode 100644 index 000000000000..eafc729b58a6 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/percentChange.ts @@ -0,0 +1,113 @@ +/** + * 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 { DataRecord } from '@superset-ui/core'; + +/** + * Rebases every non-x column of a timeseries result to its percent change + * from that column's first non-null value, restoring the view the legacy + * nvd3 "Time-series Percent Change" chart computed in the renderer: + * y' = y / y0 - 1. Columns whose baseline is 0 (or missing) yield nulls + * since a percent change from zero is undefined. + */ +export function rebaseToPercentChange( + records: DataRecord[], + xAxis: string, +): DataRecord[] { + const baselines: Record = {}; + records.forEach(record => { + Object.entries(record).forEach(([key, value]) => { + if ( + key !== xAxis && + !(key in baselines) && + typeof value === 'number' && + !Number.isNaN(value) + ) { + baselines[key] = value; + } + }); + }); + + return records.map(record => { + const rebased: DataRecord = { [xAxis]: record[xAxis] }; + Object.entries(record).forEach(([key, value]) => { + if (key === xAxis) return; + const baseline = baselines[key]; + rebased[key] = + typeof value === 'number' && + !Number.isNaN(value) && + baseline !== undefined && + baseline !== 0 + ? value / baseline - 1 + : null; + }); + return rebased; + }); +} + +export type SeriesDataPoint = [number | string, number | null]; + +/** + * Re-indexes already-rebased series data to a new baseline x. Because + * percent-change rebasing is composable, v' = (1 + v) / (1 + vk) - 1 + * derives the new view directly from the currently displayed values, + * where vk is the series value at the nearest x at or before the new + * baseline (falling back to the first non-null point). + */ +export function rebaseSeriesData( + data: SeriesDataPoint[], + baselineX: number | string, +): SeriesDataPoint[] { + // the non-null point with the largest x at or before the baseline, + // falling back to the first non-null point + let baselineValue: number | null = null; + data.forEach(([x, y]) => { + if (y == null) return; + if (x <= baselineX || baselineValue === null) { + baselineValue = y; + } + }); + if (baselineValue === null || baselineValue === -1) { + return data.map(([x]) => [x, null]); + } + const divisor = 1 + baselineValue; + return data.map(([x, y]) => [x, y == null ? null : (1 + y) / divisor - 1]); +} + +/** + * Snaps a dragged x position to the nearest available data x. Time/value + * axes report a continuous pixel-derived number, so the nearest point is + * found by numeric distance. Category axes report the exact category + * value already snapped by ECharts; there's no numeric scale to measure + * distance against, so it's only validated against the known x values + * (falling back to the first one if the pixel landed outside the axis). + */ +export function snapToNearestX( + xs: (number | string)[], + target: number | string, +): number | string | undefined { + if (xs.length === 0) return undefined; + if (typeof target === 'string') { + return xs.includes(target) ? target : xs[0]; + } + const numericXs = xs.filter((x): x is number => typeof x === 'number'); + if (numericXs.length === 0) return undefined; + return numericXs.reduce((best, x) => + Math.abs(x - target) < Math.abs(best - target) ? x : best, + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 1493aeee8970..6de6eb95b2b6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -18,6 +18,7 @@ */ /* eslint-disable camelcase */ import { invert } from 'lodash-es'; +import { rebaseToPercentChange } from './percentChange'; import { t } from '@apache-superset/core/translation'; import { AnnotationLayer, @@ -290,7 +291,7 @@ export default function transformProps( return { ...acc, [entry[0]]: entry[1] }; }, {}); const colorScale = CategoricalColorNamespace.getScale(colorScheme as string); - const rebasedData = rebaseForecastDatum(data, verboseMap); + const forecastRebasedData = rebaseForecastDatum(data, verboseMap); let xAxisLabel = getXAxisLabel(chartProps.rawFormData) as string; if ( isPhysicalColumn(chartProps.rawFormData?.x_axis) && @@ -298,6 +299,16 @@ export default function transformProps( ) { xAxisLabel = verboseMap[xAxisLabel]; } + // Restores the legacy nvd3 "Time-series Percent Change" view: every series + // rebased client-side to its percent change from the first point. The + // baseline can be re-indexed interactively via the draggable line the + // chart component installs. + const rebasePercentChange = Boolean( + (formData as { rebasePercentChange?: boolean }).rebasePercentChange, + ); + const rebasedData = rebasePercentChange + ? rebaseToPercentChange(forecastRebasedData, xAxisLabel) + : forecastRebasedData; const isHorizontal = orientation === OrientationType.Horizontal; const { totalStackedValues, thresholdValues } = extractDataTotalValues( rebasedData, @@ -349,7 +360,9 @@ export default function transformProps( const isAreaExpand = stack === StackControlsValue.Expand; const series: SeriesOption[] = []; - const forcePercentFormatter = Boolean(contributionMode || isAreaExpand); + const forcePercentFormatter = Boolean( + contributionMode || isAreaExpand || rebasePercentChange, + ); const percentFormatter = forcePercentFormatter ? getPercentFormatter(yAxisFormat) : getPercentFormatter(NumberFormats.PERCENT_2_POINT); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/percentChange.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/percentChange.test.ts new file mode 100644 index 000000000000..4dd3bbc1ad64 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/percentChange.test.ts @@ -0,0 +1,98 @@ +/** + * 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 { + rebaseToPercentChange, + rebaseSeriesData, + snapToNearestX, +} from '../../src/Timeseries/percentChange'; + +test('rebases each column to percent change from its first value', () => { + const rebased = rebaseToPercentChange( + [ + { __timestamp: 1, a: 100, b: 50 }, + { __timestamp: 2, a: 150, b: 25 }, + { __timestamp: 3, a: 200, b: null }, + ], + '__timestamp', + ); + expect(rebased).toEqual([ + { __timestamp: 1, a: 0, b: 0 }, + { __timestamp: 2, a: 0.5, b: -0.5 }, + { __timestamp: 3, a: 1, b: null }, + ]); +}); + +test('skips leading nulls when picking the baseline', () => { + const rebased = rebaseToPercentChange( + [ + { __timestamp: 1, a: null }, + { __timestamp: 2, a: 40 }, + { __timestamp: 3, a: 60 }, + ], + '__timestamp', + ); + expect(rebased[1].a).toBe(0); + expect(rebased[2].a).toBeCloseTo(0.5); +}); + +test('yields nulls when the baseline is zero', () => { + const rebased = rebaseToPercentChange( + [ + { __timestamp: 1, a: 0 }, + { __timestamp: 2, a: 10 }, + ], + '__timestamp', + ); + expect(rebased.map(r => r.a)).toEqual([null, null]); +}); + +test('re-indexing displayed data matches rebasing raw data to that point', () => { + // raw 100, 150, 200 rebased to first point: 0, 0.5, 1 + const displayed: [number, number][] = [ + [1, 0], + [2, 0.5], + [3, 1], + ]; + // re-index to x=2 (raw 150): 100/150-1, 0, 200/150-1 + const reindexed = rebaseSeriesData(displayed, 2); + expect(reindexed[0][1]).toBeCloseTo(100 / 150 - 1); + expect(reindexed[1][1]).toBeCloseTo(0); + expect(reindexed[2][1]).toBeCloseTo(200 / 150 - 1); +}); + +test('re-indexing to the current baseline is a no-op', () => { + const displayed: [number, number][] = [ + [1, 0], + [2, 0.5], + ]; + expect(rebaseSeriesData(displayed, 1)).toEqual(displayed); +}); + +test('snaps to the nearest available x', () => { + expect(snapToNearestX([10, 20, 30], 24)).toBe(20); + expect(snapToNearestX([10, 20, 30], 26)).toBe(30); + expect(snapToNearestX([], 5)).toBeUndefined(); +}); + +test('validates a category-axis x instead of coercing it to a number', () => { + // category axes report their exact (already-snapped) value on drag + expect(snapToNearestX(['a', 'b', 'c'], 'b')).toBe('b'); + // a pixel that landed outside the axis falls back to the first category + expect(snapToNearestX(['a', 'b', 'c'], 'unknown')).toBe('a'); +}); diff --git a/superset/migrations/shared/migrate_viz/processors.py b/superset/migrations/shared/migrate_viz/processors.py index 8a1a931cff8d..a4c2b05ea7c4 100644 --- a/superset/migrations/shared/migrate_viz/processors.py +++ b/superset/migrations/shared/migrate_viz/processors.py @@ -392,6 +392,13 @@ class MigrateCompareChart(TimeseriesChart): source_viz_type = "compare" target_viz_type = "echarts_timeseries_line" + def _pre_action(self) -> None: + super()._pre_action() + # Restore the percent-change view the nvd3 renderer computed + # client-side; the ECharts line chart rebases series when this + # flag is set and offers a draggable baseline to re-index. + self.data["rebase_percent_change"] = True + class MigrateAreaChart(TimeseriesChart): source_viz_type = "area" diff --git a/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py b/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py index 6107efc44fd0..2b41289616e8 100644 --- a/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py +++ b/tests/unit_tests/migrations/viz/nvd3_compare_chart_to_echarts_test.py @@ -30,6 +30,8 @@ TARGET_FORM_DATA: dict[str, Any] = { "form_data_bak": SOURCE_FORM_DATA, "viz_type": "echarts_timeseries_line", + # the percent-change view the nvd3 renderer computed client-side + "rebase_percent_change": True, } From 8cd636bcd0ea19d166dfeadd60b842374d0817f5 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 20 Jul 2026 17:17:34 -0700 Subject: [PATCH 56/84] feat(time-pivot): rebuild Time-series Period Pivot on ECharts (#42245) Co-authored-by: Claude Code --- .../src/TimePivot/EchartsTimePivot.tsx | 40 ++++ .../src/TimePivot/buildQuery.ts | 34 +++ .../src/TimePivot/controlPanel.ts | 210 ++++++++++++++++++ .../src/TimePivot/images/example-dark.jpg | Bin 0 -> 137758 bytes .../src/TimePivot/images/example.jpg | Bin 0 -> 131853 bytes .../src/TimePivot/images/thumbnail-dark.png | Bin 0 -> 25338 bytes .../src/TimePivot/images/thumbnail.png | Bin 0 -> 11158 bytes .../src/TimePivot/images/thumbnailLarge.png | Bin 0 -> 26764 bytes .../src/TimePivot/index.ts | 55 +++++ .../src/TimePivot/transformData.ts | 187 ++++++++++++++++ .../src/TimePivot/transformProps.ts | 197 ++++++++++++++++ .../src/TimePivot/types.ts | 48 ++++ .../plugins/plugin-chart-echarts/src/index.ts | 1 + .../test/TimePivot/transformData.test.ts | 108 +++++++++ .../test/TimePivot/transformProps.test.ts | 92 ++++++++ .../src/visualizations/presets/MainPreset.ts | 8 +- 16 files changed, 975 insertions(+), 5 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/EchartsTimePivot.tsx create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/buildQuery.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/controlPanel.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example-dark.jpg create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example.jpg create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail-dark.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnailLarge.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/index.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformData.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformProps.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/types.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformData.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformProps.test.ts diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/EchartsTimePivot.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/EchartsTimePivot.tsx new file mode 100644 index 000000000000..aacfb5b256d4 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/EchartsTimePivot.tsx @@ -0,0 +1,40 @@ +/** + * 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 Echart from '../components/Echart'; +import { allEventHandlers } from '../utils/eventHandlers'; +import { TimePivotChartTransformedProps } from './types'; + +export default function EchartsTimePivot( + props: TimePivotChartTransformedProps, +) { + const { height, width, echartOptions, refs, formData } = props; + + const eventHandlers = allEventHandlers(props); + + return ( + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/buildQuery.ts new file mode 100644 index 000000000000..76a788b6e694 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/buildQuery.ts @@ -0,0 +1,34 @@ +/** + * 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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; + +/** + * Mirrors the legacy NVD3TimePivotViz.query_obj: a timeseries query with + * the single metric. + */ +export default function buildQuery(formData: QueryFormData) { + const { metric } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + is_timeseries: true, + metrics: metric ? [metric] : [], + }, + ]); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/controlPanel.ts new file mode 100644 index 000000000000..6b5e045783b2 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/controlPanel.ts @@ -0,0 +1,210 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { + ControlPanelConfig, + D3_TIME_FORMAT_OPTIONS, + sections, +} from '@superset-ui/chart-controls'; + +// Control names match the legacy nvd3 chart so saved charts keep working +// without a form-data migration. nvd3-specific pixel-margin and min/max +// toggles are intentionally dropped; ECharts lays those out automatically. +const config: ControlPanelConfig = { + controlPanelSections: [ + sections.legacyTimeseriesTime, + { + label: t('Query'), + expanded: true, + controlSetRows: [ + ['metric'], + ['adhoc_filters'], + [ + { + name: 'freq', + config: { + type: 'SelectControl', + label: t('Frequency'), + default: 'W-MON', + freeForm: true, + clearable: false, + choices: [ + ['AS', t('Year (freq=AS)')], + ['52W-MON', t('52 weeks starting Monday (freq=52W-MON)')], + ['W-SUN', t('1 week starting Sunday (freq=W-SUN)')], + ['W-MON', t('1 week starting Monday (freq=W-MON)')], + ['D', t('Day (freq=D)')], + ['4W-MON', t('4 weeks (freq=4W-MON)')], + ], + description: t( + `The periodicity over which to pivot time. Each period becomes + its own overlaid series, so pick a Time Grain finer than this + frequency to see lines (e.g. Day grain with a weekly + frequency); with one data point per period each series renders + as a single dot. Users can provide "Pandas" offset aliases. + Click on the info bubble for more details on accepted "freq" + expressions.`, + ), + tooltipOnClick: () => { + window.open( + 'https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases', + '_blank', + 'noopener noreferrer', + ); + }, + }, + }, + ], + [ + { + name: 'period_limit', + config: { + type: 'TextControl', + isInt: true, + label: t('Number of periods'), + renderTrigger: true, + default: '', + description: t( + 'Show only the N most recent periods, from 1 up to the number ' + + 'of periods in the data. Leave empty to overlay them all.', + ), + }, + }, + ], + ], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'show_legend', + config: { + type: 'CheckboxControl', + label: t('Legend'), + renderTrigger: true, + default: false, + description: t('Whether to display the legend (toggles)'), + }, + }, + ], + ['color_picker'], + [ + { + name: 'line_interpolation', + config: { + type: 'SelectControl', + label: t('Line Style'), + renderTrigger: true, + choices: [ + ['linear', t('Linear')], + ['cardinal', t('Smooth')], + ['step-before', t('Step - start')], + ['step-after', t('Step - end')], + ], + default: 'linear', + description: t('Line interpolation as defined by d3.js'), + }, + }, + ], + ], + }, + { + label: t('X Axis'), + expanded: true, + controlSetRows: [ + [ + { + name: 'x_axis_label', + config: { + type: 'TextControl', + label: t('X Axis Label'), + renderTrigger: true, + default: '', + }, + }, + ], + [ + { + name: 'x_axis_format', + config: { + type: 'SelectControl', + freeForm: true, + label: t('X Axis Format'), + renderTrigger: true, + default: 'smart_date', + choices: D3_TIME_FORMAT_OPTIONS, + description: t('D3 time format for the x-axis labels'), + }, + }, + ], + ], + }, + { + label: t('Y Axis'), + expanded: true, + controlSetRows: [ + [ + { + name: 'y_axis_label', + config: { + type: 'TextControl', + label: t('Y Axis Label'), + renderTrigger: true, + default: '', + }, + }, + ], + ['y_axis_format'], + [ + { + name: 'y_log_scale', + config: { + type: 'CheckboxControl', + label: t('Y Log Scale'), + default: false, + renderTrigger: true, + description: t('Use a log scale for the Y-axis'), + }, + }, + ], + [ + { + name: 'y_axis_bounds', + config: { + type: 'BoundsControl', + label: t('Y Axis Bounds'), + renderTrigger: true, + default: [null, null], + description: t( + 'Bounds for the Y-axis. When left empty, the bounds are ' + + 'dynamically defined based on the min/max of the data. Note that ' + + "this feature will only expand the axis range. It won't " + + "narrow the data's extent.", + ), + }, + }, + ], + ], + }, + ], +}; + +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example-dark.jpg b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/example-dark.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6d92e0682b4fef33d1c86f21a1b36d76eae0ee68 GIT binary patch literal 137758 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 literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/images/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..dde48ae0246d0051a789d2cd1fa2ad2fd1ebeefc GIT binary patch literal 11158 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Ȑ_)ZL#G61vJ}s;^4Qbp=3M;m=?|CFjTGa3 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 { + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsTimePivot'), + metadata: { + behaviors: [Behavior.InteractiveChart], + category: t('Evolution'), + description: t( + 'Compares the current time period against equivalent past periods by overlaying them on a shared time axis. The current period is drawn boldly while prior periods fade with age.', + ), + exampleGallery: [{ url: example, urlDark: exampleDark }], + name: t('Time-series Period Pivot'), + tags: [t('Comparison'), t('Time'), t('Trend'), t('ECharts')], + thumbnail, + thumbnailDark, + }, + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformData.ts b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformData.ts new file mode 100644 index 000000000000..b1d21bb0af57 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformData.ts @@ -0,0 +1,187 @@ +/** + * 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 { DTTM_ALIAS } from '@superset-ui/core'; + +const DAY_MS = 24 * 60 * 60 * 1000; +const WEEKDAYS = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; + +/** + * Rolls a timestamp back to the start of its period the way pandas + * offset.rollback(normalize=True) does for the offset aliases the + * frequency control offers (AS/A, QS/Q, MS/M, W and W-XXX, D, H, T/MIN). + * The multiplier prefix (e.g. 52W-MON) does not change the anchor. + */ +export const rollback = (timestamp: number, freq: string): number => { + const match = /^\d*(AS|YS|A|Y|QS|Q|MS|M|W(?:-([A-Z]{3}))?|D|H|T|MIN)$/i.exec( + (freq || 'W-MON').trim(), + ); + const date = new Date(timestamp); + const midnight = Date.UTC( + date.getUTCFullYear(), + date.getUTCMonth(), + date.getUTCDate(), + ); + if (!match) { + return midnight; // freeform rules fall back to day boundaries + } + const unit = match[1].toUpperCase(); + if (unit === 'AS' || unit === 'YS') { + return Date.UTC(date.getUTCFullYear(), 0, 1); + } + if (unit === 'A' || unit === 'Y') { + // most recent Dec 31 at or before the timestamp + const yearEnd = Date.UTC(date.getUTCFullYear(), 11, 31); + return midnight >= yearEnd + ? yearEnd + : Date.UTC(date.getUTCFullYear() - 1, 11, 31); + } + if (unit === 'QS') { + return Date.UTC( + date.getUTCFullYear(), + Math.floor(date.getUTCMonth() / 3) * 3, + 1, + ); + } + if (unit === 'Q') { + // most recent quarter end at or before the timestamp + const quarterEndMonth = Math.floor(date.getUTCMonth() / 3) * 3 + 3; + const quarterEnd = Date.UTC(date.getUTCFullYear(), quarterEndMonth, 0); + return midnight >= quarterEnd + ? quarterEnd + : Date.UTC(date.getUTCFullYear(), quarterEndMonth - 3, 0); + } + if (unit === 'MS') { + return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1); + } + if (unit === 'M') { + // most recent month end at or before the timestamp + const monthEnd = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0); + return midnight >= monthEnd + ? monthEnd + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 0); + } + if (unit.startsWith('W')) { + const anchor = match[2] ? WEEKDAYS.indexOf(match[2].toUpperCase()) : 6; // W defaults to W-SUN + // JS getUTCDay: 0=SUN..6=SAT; WEEKDAYS index: 0=MON..6=SUN + const jsAnchor = anchor === 6 ? 0 : anchor + 1; + const diff = (date.getUTCDay() - jsAnchor + 7) % 7; + return midnight - diff * DAY_MS; + } + if (unit === 'D') { + return midnight; + } + if (unit === 'H') { + return timestamp - (timestamp % (60 * 60 * 1000)); + } + return timestamp - (timestamp % (60 * 1000)); // T / MIN +}; + +export interface TimePivotSeries { + key: string; + values: { x: number; y: number | null }[]; + rank: number; + perc: number; +} + +/** + * Ports the legacy NVD3TimePivotViz.get_data reshape: timestamps are + * bucketed into periods by the freq offset, ranked most-recent-first + * ("current", "-1", "-2", ...), shifted onto the latest period's time + * axis, and pivoted into one series per period with rank/perc metadata. + */ +export default function transformData( + records: Record[], + metricLabel: string, + freq: string, + periodLimit?: number, +): TimePivotSeries[] { + const rows = records + .filter(record => record[DTTM_ALIAS] != null) + .map(record => { + const timestamp = record[DTTM_ALIAS] as number; + return { + timestamp, + period: rollback(timestamp, freq), + value: record[metricLabel] as number | null, + }; + }); + if (rows.length === 0) { + return []; + } + + const periods = Array.from(new Set(rows.map(row => row.period))) + .sort((a, b) => b - a) + .slice( + 0, + // clamp to [1, available periods]; undefined keeps them all + periodLimit && periodLimit > 0 ? Math.floor(periodLimit) : undefined, + ); + const maxPeriod = periods[0]; + const rankOf = new Map(periods.map((period, index) => [period, index])); + const maxRank = periods.length - 1; + + const seriesOf = (rank: number) => (rank === 0 ? 'current' : `-${rank}`); + + // shift every point onto the latest period's time axis + const values = new Map>(); + const shiftedTimestamps = new Set(); + rows.forEach(({ timestamp, period, value }) => { + if (!rankOf.has(period)) { + return; // period dropped by the limit + } + const series = seriesOf(rankOf.get(period)!); + const shifted = timestamp + (maxPeriod - period); + shiftedTimestamps.add(shifted); + if (!values.has(series)) { + values.set(series, new Map()); + } + values.get(series)!.set(shifted, value); + }); + + const timestamps = Array.from(shiftedTimestamps).sort((a, b) => a - b); + // pandas pivot sorts the series labels lexicographically + const seriesLabels = Array.from(values.keys()).sort(); + + const chartData: TimePivotSeries[] = []; + seriesLabels.forEach(series => { + const seriesValues = values.get(series)!; + let nonNullCount = 0; + const points = timestamps.map(timestamp => { + let y = seriesValues.has(timestamp) ? seriesValues.get(timestamp)! : null; + if (typeof y === 'number' && Number.isNaN(y)) { + y = null; + } + if (y != null) { + nonNullCount += 1; + } + return { x: timestamp, y }; + }); + if (nonNullCount === 0) { + return; + } + const rank = series === 'current' ? 0 : Number(series.slice(1)); + chartData.push({ + key: series, + values: points, + rank, + perc: 1 - rank / (maxRank + 1), + }); + }); + return chartData; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformProps.ts new file mode 100644 index 000000000000..dc33d0542045 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/transformProps.ts @@ -0,0 +1,197 @@ +/** + * 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 { + getMetricLabel, + getNumberFormatter, + getTimeFormatter, + JsonObject, + SMART_DATE_VERBOSE_ID, +} from '@superset-ui/core'; +import type { EChartsCoreOption } from 'echarts/core'; +import { Refs } from '../types'; +import { calculateLowerLogTick } from '../utils/series'; +import transformData, { TimePivotSeries } from './transformData'; +import { + EchartsTimePivotChartProps, + TimePivotChartTransformedProps, +} from './types'; + +const DEFAULT_COLOR = { r: 0, g: 122, b: 135, a: 1 }; + +export default function transformProps( + chartProps: EchartsTimePivotChartProps, +): TimePivotChartTransformedProps { + const { width, height, formData, queriesData, theme, hooks } = chartProps; + const { + metric, + freq, + periodLimit, + colorPicker, + showLegend, + lineInterpolation, + xAxisFormat, + xAxisLabel, + yAxisLabel, + yAxisFormat, + yLogScale, + yAxisBounds, + } = formData; + const refs: Refs = {}; + const { onContextMenu, setDataMask = () => {} } = hooks; + + const metricLabel = getMetricLabel(metric ?? ''); + const records = (queriesData[0]?.data ?? []) as Record[]; + const series: TimePivotSeries[] = Array.isArray(records) + ? transformData( + records, + metricLabel, + (freq as string) || 'W-MON', + periodLimit ? Number(periodLimit) : undefined, + ) + : []; + + const { r, g, b } = colorPicker ?? DEFAULT_COLOR; + // Match the nvd3 styling: the current period is fully opaque, prior + // periods fade with their recency percentile. + const colorOf = (s: TimePivotSeries) => + `rgba(${r}, ${g}, ${b}, ${s.rank > 0 ? s.perc * 0.5 : 1})`; + + const smooth = lineInterpolation === 'cardinal'; + const step = + lineInterpolation === 'step-before' + ? 'start' + : lineInterpolation === 'step-after' + ? 'end' + : undefined; + + const valueFormatter = getNumberFormatter(yAxisFormat); + const timeFormatter = getTimeFormatter(SMART_DATE_VERBOSE_ID); + + // Draw the current period last so it paints on top of the faded priors. + const sortedSeries = [...series].sort((a, b) => b.rank - a.rank); + + const [, yMax] = yAxisBounds ?? [null, null]; + let yMin = yAxisBounds?.[0] ?? null; + // ECharts log axes only accept strictly positive values, so anchor the + // default minimum to the smallest positive value rather than letting the + // axis include zero/negative territory it cannot render. + if (yLogScale && yMin == null) { + const minPositiveValue = Math.min( + ...series.flatMap(s => + s.values + .map(({ y }) => y) + .filter((y): y is number => y != null && y > 0), + ), + ); + if (Number.isFinite(minPositiveValue)) { + yMin = calculateLowerLogTick(minPositiveValue); + } + } + + const echartOptions: EChartsCoreOption = { + grid: { + top: theme.sizeUnit * 8, + bottom: theme.sizeUnit * 8, + left: theme.sizeUnit * 4, + right: theme.sizeUnit * 6, + containLabel: true, + }, + legend: { + show: showLegend !== false, + top: 0, + data: series.map(s => s.key), + }, + xAxis: { + type: 'time', + name: xAxisLabel || undefined, + nameLocation: 'middle', + nameGap: theme.sizeUnit * 8, + axisLabel: { + color: theme.colorTextSecondary, + ...(xAxisFormat && xAxisFormat !== 'smart_date' + ? { + formatter: (value: number) => + getTimeFormatter(xAxisFormat)(value), + } + : {}), + }, + }, + yAxis: { + type: yLogScale ? 'log' : 'value', + name: yAxisLabel || undefined, + nameLocation: 'middle', + nameGap: theme.sizeUnit * 12, + min: yMin ?? undefined, + max: yMax ?? undefined, + axisLabel: { + color: theme.colorTextSecondary, + formatter: (value: number) => valueFormatter(value), + }, + }, + tooltip: { + trigger: 'axis', + confine: true, + formatter: (params: JsonObject[]) => { + const rows = params + .filter(param => param.value?.[1] != null) + .map( + param => + `${param.marker}${param.seriesName}: ${valueFormatter( + param.value[1] as number, + )}`, + ); + const axisTime = params[0]?.value?.[0]; + return [ + axisTime != null ? timeFormatter(axisTime as number) : '', + ...rows, + ].join('
'); + }, + }, + series: sortedSeries.map(s => ({ + name: s.key, + type: 'line', + smooth, + ...(step ? { step } : {}), + // a line series with one point renders nothing without its symbol; + // sparse periods (e.g. yearly data pivoted by 52 weeks) need them + showSymbol: s.values.filter(({ y }) => y != null).length <= 2, + connectNulls: false, + lineStyle: { + color: colorOf(s), + width: s.rank === 0 ? 3 : 2, + }, + itemStyle: { color: colorOf(s) }, + data: s.values.map(({ x, y }) => [x, y]), + z: s.rank === 0 ? 10 : 2, + })), + }; + + return { + width, + height, + echartOptions, + formData, + onContextMenu, + setDataMask, + selectedValues: {}, + groupby: [], + labelMap: {}, + refs, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/types.ts new file mode 100644 index 000000000000..18075445e59e --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/TimePivot/types.ts @@ -0,0 +1,48 @@ +/** + * 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 { QueryFormData, QueryFormMetric } from '@superset-ui/core'; +import { + BaseChartProps, + BaseTransformedProps, + CrossFilterTransformedProps, +} from '../types'; + +export type EchartsTimePivotFormData = QueryFormData & { + metric?: QueryFormMetric; + /** pandas-style period offset, e.g. W-MON, D, AS */ + freq?: string; + /** keep only the N most recent periods; empty keeps all */ + periodLimit?: number | string; + colorPicker?: { r: number; g: number; b: number; a: number }; + showLegend?: boolean; + lineInterpolation?: string; + xAxisFormat?: string; + xAxisLabel?: string; + yAxisLabel?: string; + yAxisFormat?: string; + yLogScale?: boolean; + yAxisBounds?: [number | null, number | null]; +}; + +export interface EchartsTimePivotChartProps extends BaseChartProps { + formData: EchartsTimePivotFormData; +} + +export type TimePivotChartTransformedProps = + BaseTransformedProps & CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index abb57d9e6e8b..fd50636e2758 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -17,6 +17,7 @@ * under the License. */ export { default as EchartsBoxPlotChartPlugin } from './BoxPlot'; +export { default as EchartsTimePivotChartPlugin } from './TimePivot'; export { default as EchartsTimeseriesChartPlugin } from './Timeseries'; export { default as EchartsAreaChartPlugin } from './Timeseries/Area'; export { default as EchartsTimeseriesBarChartPlugin } from './Timeseries/Regular/Bar'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformData.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformData.test.ts new file mode 100644 index 000000000000..cf22e578cf7b --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformData.test.ts @@ -0,0 +1,108 @@ +/** + * 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 { QueryFormData } from '@superset-ui/core'; +import buildQuery from '../../src/TimePivot/buildQuery'; +import transformData, { rollback } from '../../src/TimePivot/transformData'; + +test('buildQuery builds a timeseries query with the single metric', () => { + const formData: QueryFormData = { + datasource: '5__table', + granularity_sqla: 'ds', + time_range: 'Last quarter', + viz_type: 'time_pivot', + metric: 'sum__num', + freq: 'W-MON', + }; + const [query] = buildQuery(formData).queries; + expect(query.metrics).toEqual(['sum__num']); + expect(query.is_timeseries).toBe(true); +}); + +describe('TimePivot rollback', () => { + const wed = Date.UTC(2024, 0, 10, 15, 30); // Wednesday 2024-01-10 + test.each([ + ['W-MON', Date.UTC(2024, 0, 8)], + ['52W-MON', Date.UTC(2024, 0, 8)], + ['W-SUN', Date.UTC(2024, 0, 7)], + ['D', Date.UTC(2024, 0, 10)], + ['AS', Date.UTC(2024, 0, 1)], + ['QS', Date.UTC(2024, 0, 1)], + ['MS', Date.UTC(2024, 0, 1)], + ])('rolls back to the %s period start', (freq, expected) => { + expect(rollback(wed, freq as string)).toEqual(expected); + }); + + test('rolls back to the most recent month end for M', () => { + expect(rollback(wed, 'M')).toEqual(Date.UTC(2023, 11, 31)); + expect(rollback(Date.UTC(2024, 0, 31), 'M')).toEqual(Date.UTC(2024, 0, 31)); + }); +}); + +describe('TimePivot transformData', () => { + const mon1 = Date.UTC(2024, 0, 1); // Monday + const tue1 = Date.UTC(2024, 0, 2); + const mon2 = Date.UTC(2024, 0, 8); // next Monday + const tue2 = Date.UTC(2024, 0, 9); + + test('pivots periods onto the latest period axis with ranks', () => { + const data = transformData( + [ + { __timestamp: mon1, sum__num: 1 }, + { __timestamp: tue1, sum__num: 2 }, + { __timestamp: mon2, sum__num: 3 }, + { __timestamp: tue2, sum__num: 4 }, + ], + 'sum__num', + 'W-MON', + ); + expect(data.map(series => series.key)).toEqual(['-1', 'current']); + const previous = data[0]; + const current = data[1]; + expect(previous.rank).toEqual(1); + expect(previous.perc).toEqual(0.5); + expect(current.rank).toEqual(0); + expect(current.perc).toEqual(1); + // the older week is shifted onto the current week's timestamps + expect(previous.values).toEqual([ + { x: mon2, y: 1 }, + { x: tue2, y: 2 }, + ]); + expect(current.values).toEqual([ + { x: mon2, y: 3 }, + { x: tue2, y: 4 }, + ]); + }); + + test('returns an empty list for empty input', () => { + expect(transformData([], 'sum__num', 'W-MON')).toEqual([]); + }); +}); + +test('limits to the N most recent periods, clamped to what exists', () => { + const WEEK = 7 * 24 * 3600 * 1000; + const M1 = 1578268800000; + const records = [0, 1, 2, 3].map(i => ({ + __timestamp: M1 + i * WEEK, + m: 10 + i, + })); + const limited = transformData(records, 'm', 'W-MON', 2); + expect(limited.map(s => s.key).sort()).toEqual(['-1', 'current']); + // clamped: asking for more than available keeps them all + expect(transformData(records, 'm', 'W-MON', 99)).toHaveLength(4); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformProps.test.ts new file mode 100644 index 000000000000..a83d261d1e9d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/TimePivot/transformProps.test.ts @@ -0,0 +1,92 @@ +/** + * 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 { ChartProps, SqlaFormData, VizType } from '@superset-ui/core'; +import { supersetTheme } from '@apache-superset/core/theme'; +import transformProps from '../../src/TimePivot/transformProps'; +import { EchartsTimePivotChartProps } from '../../src/TimePivot/types'; + +const WEEK = 7 * 24 * 3600 * 1000; +// Mondays (UTC): 2020-01-06 and 2020-01-13 +const MONDAY_1 = 1578268800000; +const MONDAY_2 = MONDAY_1 + WEEK; + +const formData: SqlaFormData = { + datasource: '5__table', + viz_type: VizType.TimePivot, + metric: 'sum__num', + freq: 'W-MON', + colorPicker: { r: 0, g: 122, b: 135, a: 1 }, +}; + +const chartProps = (overrides: Partial = {}) => + new ChartProps({ + width: 800, + height: 400, + formData: { ...formData, ...overrides }, + theme: supersetTheme, + queriesData: [ + { + data: [ + { __timestamp: MONDAY_1, sum__num: 10 }, + { __timestamp: MONDAY_2, sum__num: 20 }, + ], + }, + ], + hooks: {}, + }) as unknown as EchartsTimePivotChartProps; + +test('pivots periods into one line series each, current on top', () => { + const { echartOptions } = transformProps(chartProps()); + const { series } = echartOptions as any; + + expect(series).toHaveLength(2); + // drawn prior-first so "current" paints on top + expect(series.map((s: any) => s.name)).toEqual(['-1', 'current']); + + const current = series[1]; + expect(current.type).toBe('line'); + expect(current.lineStyle.color).toBe('rgba(0, 122, 135, 1)'); + // prior period shifted onto the current period's axis, faded + const prior = series[0]; + expect(prior.data[0][0]).toBe(MONDAY_2); + expect(prior.lineStyle.color).toMatch(/rgba\(0, 122, 135, 0\.2/); +}); + +test('honors log scale and y-axis bounds', () => { + const { echartOptions } = transformProps( + chartProps({ yLogScale: true, yAxisBounds: [1, 100] }), + ); + const { yAxis } = echartOptions as any; + expect(yAxis.type).toBe('log'); + expect(yAxis.min).toBe(1); + expect(yAxis.max).toBe(100); +}); + +test('handles an empty result without crashing', () => { + const props = new ChartProps({ + width: 800, + height: 400, + formData, + theme: supersetTheme, + queriesData: [{ data: [] }], + hooks: {}, + }) as unknown as EchartsTimePivotChartProps; + const { echartOptions } = transformProps(props); + expect((echartOptions as any).series).toEqual([]); +}); diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index 5aa9c9b8576a..99b4150a3c47 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -33,10 +33,7 @@ import RoseChartPlugin from '@superset-ui/plugin-chart-rose'; import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; import WorldMapChartPlugin from '@superset-ui/plugin-chart-world-map'; -import { - BulletChartPlugin, - TimePivotChartPlugin, -} from '@superset-ui/preset-chart-nvd3'; +import { BulletChartPlugin } from '@superset-ui/preset-chart-nvd3'; import { DeckGLChartPreset } from '@superset-ui/preset-chart-deckgl'; import ScatterMapChartPlugin from '@superset-ui/plugin-chart-point-cluster-map'; import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; @@ -67,6 +64,7 @@ import { BigNumberPeriodOverPeriodChartPlugin, EchartsHeatmapChartPlugin, EchartsGanttChartPlugin, + EchartsTimePivotChartPlugin, } from '@superset-ui/plugin-chart-echarts'; import { SelectFilterPlugin, @@ -137,7 +135,7 @@ export default class MainPreset extends Preset { new PivotTableChartPluginV2().configure({ key: VizType.PivotTable }), new RoseChartPlugin().configure({ key: VizType.Rose }), new TableChartPlugin().configure({ key: VizType.Table }), - new TimePivotChartPlugin().configure({ key: VizType.TimePivot }), + new EchartsTimePivotChartPlugin().configure({ key: VizType.TimePivot }), new TimeTableChartPlugin().configure({ key: VizType.TimeTable }), new WordCloudChartPlugin().configure({ key: VizType.WordCloud }), new WorldMapChartPlugin().configure({ key: VizType.WorldMap }), From 05ce4d41bfe5375ff7ca6431b3a2292b93a3b324 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Mon, 20 Jul 2026 18:04:21 -0700 Subject: [PATCH 57/84] feat(bullet): rebuild the Bullet Chart on ECharts (#42225) Co-authored-by: Claude Code --- .../src/Bullet/EchartsBullet.tsx | 38 +++ .../src/Bullet/buildQuery.ts | 38 +++ .../src/Bullet/controlPanel.tsx | 151 +++++++++ .../src/Bullet/images/example-dark.jpg | Bin 0 -> 22213 bytes .../src/Bullet/images/example.jpg | Bin 0 -> 23045 bytes .../src/Bullet/images/thumbnail-dark.png | Bin 0 -> 3653 bytes .../src/Bullet/images/thumbnail.png | Bin 0 -> 978 bytes .../src/Bullet/images/thumbnailLarge.png | Bin 0 -> 2362 bytes .../plugin-chart-echarts/src/Bullet/index.ts | 55 ++++ .../src/Bullet/transformProps.ts | 290 ++++++++++++++++++ .../plugin-chart-echarts/src/Bullet/types.ts | 53 ++++ .../plugin-chart-echarts/src/Bullet/utils.ts | 37 +++ .../plugins/plugin-chart-echarts/src/index.ts | 1 + .../test/Bullet/buildQuery.test.ts | 48 +++ .../test/Bullet/transformProps.test.ts | 186 +++++++++++ .../src/visualizations/presets/MainPreset.ts | 4 +- 16 files changed, 899 insertions(+), 2 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/EchartsBullet.tsx create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/buildQuery.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/controlPanel.tsx create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example-dark.jpg create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example.jpg create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail-dark.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnailLarge.png create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/index.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/types.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Bullet/utils.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/Bullet/buildQuery.test.ts create mode 100644 superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/EchartsBullet.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/EchartsBullet.tsx new file mode 100644 index 000000000000..1ab8fb64237d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/EchartsBullet.tsx @@ -0,0 +1,38 @@ +/** + * 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 Echart from '../components/Echart'; +import { allEventHandlers } from '../utils/eventHandlers'; +import { BulletChartTransformedProps } from './types'; + +export default function EchartsBullet(props: BulletChartTransformedProps) { + const { height, width, echartOptions, refs, formData } = props; + + const eventHandlers = allEventHandlers(props); + + return ( + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/buildQuery.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/buildQuery.ts new file mode 100644 index 000000000000..5750475ff42d --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/buildQuery.ts @@ -0,0 +1,38 @@ +/** + * 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 { + buildQueryContext, + ensureIsArray, + QueryFormData, +} from '@superset-ui/core'; + +/** + * A single ungrouped aggregate of the selected metric, mirroring the query + * the legacy nvd3 bullet chart issued. + */ +export default function buildQuery(formData: QueryFormData) { + const { metric, groupby } = formData; + return buildQueryContext(formData, baseQueryObject => [ + { + ...baseQueryObject, + columns: ensureIsArray(groupby), + metrics: metric ? [metric] : [], + }, + ]); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/controlPanel.tsx new file mode 100644 index 000000000000..14790e6e7364 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/controlPanel.tsx @@ -0,0 +1,151 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { ControlPanelConfig } from '@superset-ui/chart-controls'; + +// Control names match the legacy nvd3 bullet chart so saved charts keep +// working without a form-data migration. +const config: ControlPanelConfig = { + controlPanelSections: [ + { + label: t('Query'), + expanded: true, + controlSetRows: [['metric'], ['groupby'], ['adhoc_filters']], + }, + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + { + name: 'ranges', + config: { + type: 'TextControl', + label: t('Ranges'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated numeric thresholds to shade as background ranges, e.g. 100,200,300', + ), + }, + }, + { + name: 'range_labels', + config: { + type: 'TextControl', + label: t('Range labels'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated text labels, one per range value', + ), + visibility: ({ controls }) => Boolean(controls?.ranges?.value), + }, + }, + ], + [ + { + name: 'markers', + config: { + type: 'TextControl', + label: t('Markers'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated numeric values to mark with triangles, e.g. 150,250', + ), + }, + }, + { + name: 'marker_labels', + config: { + type: 'TextControl', + label: t('Marker labels'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated text labels, one per marker value', + ), + visibility: ({ controls }) => Boolean(controls?.markers?.value), + }, + }, + ], + [ + { + name: 'marker_lines', + config: { + type: 'TextControl', + label: t('Marker lines'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated numeric values to mark with vertical lines, e.g. 250', + ), + }, + }, + { + name: 'marker_line_labels', + config: { + type: 'TextControl', + label: t('Marker line labels'), + default: '', + renderTrigger: true, + description: t( + 'Comma-separated text labels, one per marker line value', + ), + visibility: ({ controls }) => + Boolean(controls?.marker_lines?.value), + }, + }, + ], + [ + { + name: 'show_labels', + config: { + type: 'CheckboxControl', + label: t('Show labels'), + renderTrigger: true, + default: false, + description: t( + 'Print range and marker labels on the chart instead of showing tooltips on hover', + ), + }, + }, + ], + [ + { + name: 'show_legend', + config: { + type: 'CheckboxControl', + label: t('Show legend'), + renderTrigger: true, + default: false, + description: t( + 'List every range, marker and marker line in a legend; clicking an entry toggles it on the chart', + ), + }, + }, + ], + ['y_axis_format'], + ], + }, + ], +}; + +export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example-dark.jpg b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/example-dark.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b6cf2a7626a8c18d078e58c148efa7e9f5be664 GIT binary patch literal 22213 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 gZr$u+U>5_s7}&+YE(Ufnu#16R4E%4x0Aut&0A$ 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 PIIw~LM{R(MHt|0I{|a^o literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail-dark.png b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d67e71c281d556ff553d061f6f1ba95ba8f01b26 GIT binary patch literal 3653 zcmeHKX;4#H8hz-DrV#}P22f;hK!buTp&_UUsNg~a5lqvF0g<4hgct-uNJ9wP>L4KL zQGv#g#H9g|C1MC`AW*U+L?IFrmLRJj0SO6G)9B1xYL$Cn${ z&ggXY*)R{&Z7kN~3j9RO?wUupmV0f4>W2t@kNKYix%1Tf~$hwKGMpN#%t`3FqD zH23QweMVXzAF`qr^6S|6c-zk${?gpP8}Fy8_Kz@qe*OO*2?CgWmTW!-@SAk-p2L3u z?{6&V2RDSjsOX0@LT|RX!l`x;Xmwp(ofIz|m*wWwpo`B!zVHC;)fj%R!rxMNhyPhE zk5CsEm#bcP&Va=r`=^&xyy6(D`;hnqV}G_0cxy;PH>fzLEVV> z;=8vnKj*{RUi0t(<;{yAnjt5gZFb0FGVWY5x>z4$k4vuUr>}`$A}>Un_KYp818ViA zG)Wn%CFOlbYsv@*+@crfMILWyx)(cJ`e3`v)wMp=&?;q73ND8vSSe9Z@zR{n5>6&E zDbf8S4|^N0AOhE#QJOICrDlzY2cMwq%5~$m8^?!g=m}nmHi#F_gz!BUZBq>$(xa-J z7nC_WBS*vPg{q$xKI z6t~KENW=C*!3PxA-Ac*9!Kf$#yXnSg&T!K+Fv}f&)~3eB>3tFIi?ZtcjIA`{gwTW5 zrPCK6DK0O9f77p!NTbauA)Kb~TTb1>`?>I~a8?XNMU615lta&U=)d&_wTsGs;}8eW z6A&h{a=Zmn6x9e}Lv3YykgD_IF3czh%tQ~UwzKlpcP5q?4$JRRco5Ox%{0fZyI?#q zF;PTxzro9iut2uw$O!AMGEdu0effLfe*aFwB+7u#YV0CpDlsHLCE%s0n%d*laOrZ0cbl?i~EZF|aPTe)JcY zolr$ilBQWhmJ%E?(*1_;>!Oj4F?y2kJlApXaiaq@&dJDu5o=~_+#iSARtXPF0-Y8) z`=C5El>lc-gh+b3Y2@*Hc@9 z=y|tq8QTGUWOi;RC{6IggsF0;(E1(szd^AyPqmH9q-g0cv(H1-*74UKs0Op_`3kIZRj z|96iyw&8-Gwj%Yo=UvkieX3|;Kvk9KroZo1-e@0p&w6fb z5RSN%cLrHW=gNkI8IImR41|Gt15Zi|esFxgL14yI(|%e+tO&lWXzv8_Dp3ygPpR;jEQ;k2JS)=(Q#Ub;>26p}%2r9Q&Oe9i8?> zzvZyAUWb|C@&oBTTJuo_NjanG-d31BtUP~#CW4+4!XQG|8Sd6$ouN}nA@hVA9cP{= zrIr(CmKT~j#HO_N65*_|@gPQ}S*atS_IA#_T<*hrXV7413-VHv0J?{Yq0Ou552bNr zP@V1K1Y@vUkz8D|^z{=#$yN9lnPqtI(9Ms~NLxaQ;++`G)~CpJXhs`tUedsC@)9Wr z22P%038hBo+=^?`a0h)VczNAma@h)Ici30F;iHAqZ%I~IX9mxd;hFs^BP}$*99MM{ zls>0FrIh=koTmdeIG=W~!R-b9i|%{>@%t0lv;I%eC;mPDCkQIKpjw%+l}d*27IuM- O0o)ECT#NRfyz~?9bbN3C literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/images/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..38e9a733cf3a8fd34f1cf48c5727cb5883b66585 GIT binary patch literal 978 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 literal 0 HcmV?d00001 diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/index.ts new file mode 100644 index 000000000000..8e4d8178ec18 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/index.ts @@ -0,0 +1,55 @@ +/** + * 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 { t } from '@apache-superset/core/translation'; +import { Behavior } from '@superset-ui/core'; +import buildQuery from './buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from './transformProps'; +import thumbnail from './images/thumbnail.png'; +import thumbnailDark from './images/thumbnail-dark.png'; +import example from './images/example.jpg'; +import exampleDark from './images/example-dark.jpg'; +import { EchartsBulletFormData, EchartsBulletChartProps } from './types'; +import { EchartsChartPlugin } from '../types'; + +export default class EchartsBulletChartPlugin extends EchartsChartPlugin< + EchartsBulletFormData, + EchartsBulletChartProps +> { + constructor() { + super({ + buildQuery, + controlPanel, + loadChart: () => import('./EchartsBullet'), + metadata: { + behaviors: [Behavior.InteractiveChart], + category: t('KPI'), + description: t( + 'Showcases the progress of a single metric against a given target. The higher the fill, the closer the metric is to the target.', + ), + exampleGallery: [{ url: example, urlDark: exampleDark }], + name: t('Bullet Chart'), + tags: [t('Business'), t('Report'), t('ECharts')], + thumbnail, + thumbnailDark, + }, + transformProps, + }); + } +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts new file mode 100644 index 000000000000..859f09bfe4cd --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts @@ -0,0 +1,290 @@ +/** + * 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 { + ensureIsArray, + getColumnLabel, + getMetricLabel, + getNumberFormatter, + JsonObject, + sanitizeHtml, +} from '@superset-ui/core'; +import type { EChartsCoreOption } from 'echarts/core'; +import { Refs } from '../types'; +import { EchartsBulletChartProps, BulletChartTransformedProps } from './types'; +import { tokenizeToNumericArray, tokenizeToStringArray } from './utils'; + +const MEASURE_BAR_FRACTION = 0.28; + +export default function transformProps( + chartProps: EchartsBulletChartProps, +): BulletChartTransformedProps { + const { width, height, formData, queriesData, theme, hooks } = chartProps; + const { + metric, + groupby, + ranges: rawRanges, + rangeLabels: rawRangeLabels, + markers: rawMarkers, + markerLabels: rawMarkerLabels, + markerLines: rawMarkerLines, + markerLineLabels: rawMarkerLineLabels, + yAxisFormat, + showLabels, + showLegend, + } = formData; + const refs: Refs = {}; + const { onContextMenu, setDataMask = () => {} } = hooks; + + const metricLabel = getMetricLabel(metric ?? ''); + const records = (queriesData[0]?.data ?? []) as JsonObject[]; + const groupbyLabels = ensureIsArray(groupby).map(getColumnLabel); + const grouped = groupbyLabels.length > 0; + // one bullet row per group value; ungrouped charts keep a single row + const rows = grouped ? records : records.slice(0, 1); + const categories = grouped + ? rows.map(row => groupbyLabels.map(l => String(row[l])).join(', ')) + : ['']; + const measures = rows.map(row => Number(row?.[metricLabel] ?? 0)); + if (measures.length === 0) measures.push(0); + // the largest row drives the default band and the axis extent + const measure = Math.max(...measures); + + // Match the legacy default: a single band up to 110% of the measure. + const ranges = tokenizeToNumericArray(rawRanges) ?? [0, measure * 1.1]; + const rangeLabels = tokenizeToStringArray(rawRangeLabels) ?? []; + const markers = tokenizeToNumericArray(rawMarkers) ?? []; + const markerLabels = tokenizeToStringArray(rawMarkerLabels) ?? []; + const markerLines = tokenizeToNumericArray(rawMarkerLines) ?? []; + const markerLineLabels = tokenizeToStringArray(rawMarkerLineLabels) ?? []; + + const formatter = getNumberFormatter(yAxisFormat); + + const axisMin = Math.min( + 0, + ...measures, + ...ranges, + ...markers, + ...markerLines, + ); + let axisMax = Math.max(...measures, ...ranges, ...markers, ...markerLines); + if (axisMax === axisMin) { + // All values identical (e.g. an empty result measuring 0) would produce a + // zero-width axis domain; expand it so the chart still renders. + axisMax = axisMin + (Math.abs(axisMin) || 1); + } + + // Nested qualitative bands like d3-bullet: draw from the largest threshold + // down so smaller (darker) bands paint on top. + const bandFills = [ + theme.colorFillQuaternary, + theme.colorFillTertiary, + theme.colorFillSecondary, + theme.colorFill, + ]; + const sortedRanges = [...ranges] + .map((value, i) => ({ value, label: rangeLabels[i] })) + .filter(({ value }) => value > axisMin) + .sort((a, b) => b.value - a.value); + const bands = sortedRanges.map(({ value, label }, i) => ({ + name: label, + itemStyle: { + color: bandFills[Math.min(i, bandFills.length - 1)], + }, + label: { show: false }, + // markArea item: [{start}, {end}] + coords: [{ xAxis: axisMin }, { xAxis: value }], + })); + + // Resolve by position rather than `indexOf(value)` so duplicate marker or + // marker-line values each keep their own label instead of collapsing onto + // the first match. + const markerLabelAt = (index: number, labels: string[], values: number[]) => + labels[index] ? labels[index] : formatter(values[index]); + + // The measure bar spans MEASURE_BAR_FRACTION of the category band + // (roughly the grid height), so a percentage symbolOffset lands inside a + // tall bar. Compute pixels: half the bar plus a small gap below it. + const gridHeight = Math.max(height - theme.sizeUnit * 10, 40); + const rowHeight = gridHeight / categories.length; + const markerOffsetPx = Math.round( + (MEASURE_BAR_FRACTION / 2) * rowHeight + 12, + ); + + const rangeName = (value: number, i: number) => + rangeLabels[i] + ? `${rangeLabels[i]}: \u2264 ${formatter(value)}` + : `\u2264 ${formatter(value)}`; + const markerName = (value: number, i: number, labels: string[]) => + labels[i] ? `${labels[i]}: ${formatter(value)}` : String(formatter(value)); + + const echartOptions: EChartsCoreOption = { + animation: false, + // Optional legend listing every range, marker and line as a toggleable + // named entry; each is its own series so ECharts handles the toggling. + legend: { show: Boolean(showLegend), top: 0 }, + grid: { + top: showLegend ? theme.sizeUnit * 10 : theme.sizeUnit * 2, + bottom: theme.sizeUnit * 6, + left: theme.sizeUnit * 2, + right: theme.sizeUnit * 2, + containLabel: true, + }, + xAxis: { + type: 'value', + min: axisMin, + max: axisMax, + axisLabel: { + color: theme.colorTextSecondary, + formatter: (value: number) => formatter(value), + }, + splitLine: { show: false }, + }, + yAxis: { + type: 'category', + data: categories, + axisLine: { show: false }, + axisTick: { show: false }, + axisLabel: { show: grouped, color: theme.colorTextSecondary }, + }, + tooltip: { + confine: true, + show: !showLabels, + }, + series: [ + { + name: metricLabel, + type: 'bar', + data: measures, + barWidth: `${MEASURE_BAR_FRACTION * 100}%`, + itemStyle: { color: theme.colorPrimary }, + tooltip: { + formatter: (params?: { dataIndex?: number }) => { + const i = params?.dataIndex ?? 0; + const prefix = grouped ? `${categories[i]} \u2014 ` : ''; + return sanitizeHtml( + `${prefix}${metricLabel}: ${formatter(measures[i])}`, + ); + }, + }, + z: 10, + markArea: { + silent: true, + data: bands.map(band => [ + { + name: band.name, + itemStyle: band.itemStyle, + label: band.label, + ...band.coords[0], + }, + band.coords[1], + ]), + }, + }, + // Invisible hover targets at each range threshold: per-item tooltips + // (markArea is not reliably hoverable), optional on-chart labels, and a + // named legend entry each. + ...ranges.map((value, index) => ({ + name: rangeName(value, index), + type: 'scatter', + data: categories.map((_, row) => ({ + value: [value, row], + tooltip: { + formatter: () => + sanitizeHtml( + `${rangeLabels[index] || ''} \u2264 ${formatter(value)}`, + ), + }, + label: { + show: + Boolean(showLabels) && + Boolean(rangeLabels[index]) && + row === categories.length - 1, + position: 'top', + color: theme.colorTextSecondary, + formatter: () => String(rangeLabels[index] || ''), + }, + })), + symbol: 'rect', + symbolSize: [14, 40], + itemStyle: { color: 'transparent' }, + z: 15, + })), + ...markers.map((value, index) => ({ + name: markerName(value, index, markerLabels), + type: 'scatter', + data: categories.map((_, row) => ({ + value: [value, row], + tooltip: { + formatter: () => + sanitizeHtml( + `${markerLabelAt(index, markerLabels, markers)}: ${formatter(value)}`, + ), + }, + })), + symbol: 'triangle', + symbolSize: 14, + symbolOffset: [0, markerOffsetPx], + label: { + show: Boolean(showLabels), + position: 'bottom', + color: theme.colorTextSecondary, + formatter: () => String(markerLabelAt(index, markerLabels, markers)), + }, + itemStyle: { color: theme.colorText }, + z: 20, + })), + // Marker lines as their own empty series so each gets a legend entry + // whose toggle shows and hides the line. + ...markerLines.map((value, index) => ({ + name: markerName(value, index, markerLineLabels), + type: 'line', + data: [], + markLine: { + silent: true, + symbol: 'none', + data: [ + { + xAxis: value, + label: { + position: 'insideEndTop', + formatter: () => + String(markerLabelAt(index, markerLineLabels, markerLines)), + color: theme.colorTextSecondary, + }, + lineStyle: { color: theme.colorText, type: 'solid', width: 2 }, + }, + ], + }, + })), + ], + }; + + return { + width, + height, + echartOptions, + formData, + onContextMenu, + setDataMask, + selectedValues: {}, + groupby: [], + labelMap: {}, + refs, + }; +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/types.ts new file mode 100644 index 000000000000..d57e8ab7a2fa --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/types.ts @@ -0,0 +1,53 @@ +/** + * 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 { + QueryFormColumn, + QueryFormData, + QueryFormMetric, +} from '@superset-ui/core'; +import { + BaseChartProps, + BaseTransformedProps, + CrossFilterTransformedProps, +} from '../types'; + +export type EchartsBulletFormData = QueryFormData & { + metric?: QueryFormMetric; + /** split into one bullet row per group value */ + groupby?: QueryFormColumn[]; + /** Comma-separated threshold values shaded as background bands */ + ranges?: string; + rangeLabels?: string; + /** Comma-separated values drawn as point markers */ + markers?: string; + markerLabels?: string; + /** Comma-separated values drawn as vertical lines */ + markerLines?: string; + markerLineLabels?: string; + yAxisFormat?: string; + showLabels?: boolean; + showLegend?: boolean; +}; + +export interface EchartsBulletChartProps extends BaseChartProps { + formData: EchartsBulletFormData; +} + +export type BulletChartTransformedProps = + BaseTransformedProps & CrossFilterTransformedProps; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/utils.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/utils.ts new file mode 100644 index 000000000000..aaac55f315d8 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/utils.ts @@ -0,0 +1,37 @@ +/** + * 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. + */ +// Ported from the nvd3 bullet chart so saved comma-separated control values +// keep their exact semantics. +export function tokenizeToNumericArray(value?: string): number[] | null { + if (!value?.trim()) return null; + // Lenient by design: this runs on every keystroke, so partial input like + // "50," must not throw. Empty and non-numeric tokens are dropped. + const numbers = value + .split(',') + .map(token => token.trim()) + .filter(token => token !== '') + .map(token => Number(token)) + .filter(n => !Number.isNaN(n)); + return numbers.length ? numbers : null; +} + +export function tokenizeToStringArray(value?: string): string[] | null { + if (!value?.trim()) return null; + return value.split(',').map(token => token.trim()); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index fd50636e2758..2b712c042ddc 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -18,6 +18,7 @@ */ export { default as EchartsBoxPlotChartPlugin } from './BoxPlot'; export { default as EchartsTimePivotChartPlugin } from './TimePivot'; +export { default as EchartsBulletChartPlugin } from './Bullet'; export { default as EchartsTimeseriesChartPlugin } from './Timeseries'; export { default as EchartsAreaChartPlugin } from './Timeseries/Area'; export { default as EchartsTimeseriesBarChartPlugin } from './Timeseries/Regular/Bar'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/buildQuery.test.ts new file mode 100644 index 000000000000..fa2c0c0a540f --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/buildQuery.test.ts @@ -0,0 +1,48 @@ +/** + * 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 buildQuery from '../../src/Bullet/buildQuery'; + +test('issues a single ungrouped aggregate of the metric', () => { + const [query] = buildQuery({ + datasource: '5__table', + viz_type: 'bullet', + metric: 'sum__num', + }).queries; + expect(query.metrics).toEqual(['sum__num']); + expect(query.columns).toEqual([]); +}); + +test('issues no metrics when none is selected', () => { + const [query] = buildQuery({ + datasource: '5__table', + viz_type: 'bullet', + }).queries; + expect(query.metrics).toEqual([]); +}); + +test('passes the groupby through as query columns', () => { + const [query] = buildQuery({ + datasource: '5__table', + viz_type: 'bullet', + metric: 'sum__num', + groupby: ['state'], + }).queries; + expect(query.columns).toEqual(['state']); + expect(query.metrics).toEqual(['sum__num']); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts new file mode 100644 index 000000000000..b5828ac7da98 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts @@ -0,0 +1,186 @@ +/** + * 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 { ChartProps, SqlaFormData, VizType } from '@superset-ui/core'; +import { supersetTheme } from '@apache-superset/core/theme'; +import transformProps from '../../src/Bullet/transformProps'; +import { EchartsBulletChartProps } from '../../src/Bullet/types'; + +const formData: SqlaFormData = { + datasource: '5__table', + viz_type: VizType.Bullet, + metric: 'sum__num', + ranges: '100,200,300', + rangeLabels: 'low,mid,high', + markers: '150', + markerLabels: 'goal', + markerLines: '250', + markerLineLabels: 'stretch', +}; + +const chartProps = (overrides: Partial = {}) => + new ChartProps({ + width: 800, + height: 200, + formData: { ...formData, ...overrides }, + theme: supersetTheme, + queriesData: [{ data: [{ sum__num: 120 }] }], + hooks: {}, + }) as unknown as EchartsBulletChartProps; + +test('renders the measure bar with markers, marker lines and range bands', () => { + const { echartOptions } = transformProps(chartProps()); + const { series } = echartOptions as any; + + // measure bar + expect(series[0].type).toBe('bar'); + expect(series[0].data).toEqual([120]); + + // marker lines are their own named series so the legend can toggle them + const lineSeries = series.filter((x: any) => x.type === 'line'); + expect(lineSeries).toHaveLength(1); + expect(lineSeries[0].markLine.data[0].xAxis).toBe(250); + + // nested range bands, largest drawn first + const bandEnds = series[0].markArea.data.map((d: any) => d[1].xAxis); + expect(bandEnds).toEqual([300, 200, 100]); + + // triangle markers, one named series each + const markerSeries = series.filter((x: any) => x.symbol === 'triangle'); + expect(markerSeries).toHaveLength(1); + expect(markerSeries[0].data[0].value).toEqual([150, 0]); + + // axis spans all values + expect((echartOptions as any).xAxis.max).toBe(300); + + // markers sit below the bar (pixel offset); legend off unless enabled + const marker = series.find((x: any) => x.symbol === 'triangle'); + expect(marker.symbolOffset[1]).toBeGreaterThan(0); + expect((echartOptions as any).legend.show).toBe(false); + + // range thresholds get their own named hover-target series + const rangeSeries = series.filter((x: any) => x.symbol === 'rect'); + const rangeTips = rangeSeries.map((x: any) => x.data[0].tooltip.formatter()); + expect(rangeTips.join(' ')).toContain('low'); + expect(rangeTips.join(' ')).toContain('high'); +}); + +test('defaults the band to 110% of the measure when no ranges are set', () => { + const { echartOptions } = transformProps( + chartProps({ + ranges: '', + rangeLabels: '', + markers: '', + markerLabels: '', + markerLines: '', + markerLineLabels: '', + }), + ); + const { series } = echartOptions as any; + expect(series[0].data).toEqual([120]); + const bandEnds = series[0].markArea.data.map((d: any) => d[1].xAxis); + expect(bandEnds).toEqual([120 * 1.1]); + expect(series.filter((x: any) => x.symbol === 'triangle')).toHaveLength(0); +}); + +test('keeps distinct labels for duplicate marker and marker-line values', () => { + const { echartOptions } = transformProps( + chartProps({ + markers: '150,150', + markerLabels: 'goal,forecast', + markerLines: '250,250', + markerLineLabels: 'stretch,ceiling', + }), + ); + const { series } = echartOptions as any; + + const markLineLabels = series + .filter((x: any) => x.type === 'line') + .map((x: any) => x.markLine.data[0].label.formatter()); + expect(markLineLabels).toEqual(['stretch', 'ceiling']); + + const markerTooltips = series + .filter((x: any) => x.symbol === 'triangle') + .map((x: any) => x.data[0].tooltip.formatter()); + expect(markerTooltips[0]).toContain('goal'); + expect(markerTooltips[1]).toContain('forecast'); +}); + +test('sanitizes HTML in metric and marker labels before rendering tooltips', () => { + const { echartOptions } = transformProps( + chartProps({ + markers: '150', + markerLabels: '', + }), + ); + const { series } = echartOptions as any; + + expect(series[0].tooltip.formatter()).not.toContain('onerror'); + expect(series[2].data[0].tooltip.formatter()).not.toContain('onerror'); +}); + +test('handles an empty query result without crashing', () => { + const props = new ChartProps({ + width: 800, + height: 200, + formData, + theme: supersetTheme, + queriesData: [{ data: [] }], + hooks: {}, + }) as unknown as EchartsBulletChartProps; + const { echartOptions } = transformProps(props); + expect((echartOptions as any).series[0].data).toEqual([0]); + // the axis domain must not collapse to zero width + const { min, max } = (echartOptions as any).xAxis; + expect(max).toBeGreaterThan(min); +}); + +test('splits into one bullet row per group value with shared markers', () => { + const props = new ChartProps({ + width: 800, + height: 400, + formData: { ...formData, groupby: ['state'] }, + theme: supersetTheme, + queriesData: [ + { + data: [ + { state: 'CA', sum__num: 120 }, + { state: 'NY', sum__num: 90 }, + ], + }, + ], + hooks: {}, + }) as unknown as EchartsBulletChartProps; + const { echartOptions } = transformProps(props); + const { series, yAxis } = echartOptions as any; + + // one category and one bar value per group + expect(yAxis.data).toEqual(['CA', 'NY']); + expect(yAxis.axisLabel.show).toBe(true); + expect(series[0].data).toEqual([120, 90]); + + // the same marker repeats on every row + const marker = series.find((x: any) => x.symbol === 'triangle'); + expect(marker.data.map((d: any) => d.value)).toEqual([ + [150, 0], + [150, 1], + ]); + + // grouped bar tooltips name the row + expect(series[0].tooltip.formatter({ dataIndex: 1 })).toContain('NY'); +}); diff --git a/superset-frontend/src/visualizations/presets/MainPreset.ts b/superset-frontend/src/visualizations/presets/MainPreset.ts index 99b4150a3c47..9970096dae18 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.ts +++ b/superset-frontend/src/visualizations/presets/MainPreset.ts @@ -33,7 +33,6 @@ import RoseChartPlugin from '@superset-ui/plugin-chart-rose'; import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; import WorldMapChartPlugin from '@superset-ui/plugin-chart-world-map'; -import { BulletChartPlugin } from '@superset-ui/preset-chart-nvd3'; import { DeckGLChartPreset } from '@superset-ui/preset-chart-deckgl'; import ScatterMapChartPlugin from '@superset-ui/plugin-chart-point-cluster-map'; import { CartodiagramPlugin } from '@superset-ui/plugin-chart-cartodiagram'; @@ -65,6 +64,7 @@ import { EchartsHeatmapChartPlugin, EchartsGanttChartPlugin, EchartsTimePivotChartPlugin, + EchartsBulletChartPlugin, } from '@superset-ui/plugin-chart-echarts'; import { SelectFilterPlugin, @@ -110,7 +110,7 @@ export default class MainPreset extends Preset { key: VizType.BigNumberTotal, }), new EchartsBoxPlotChartPlugin().configure({ key: VizType.BoxPlot }), - new BulletChartPlugin().configure({ key: VizType.Bullet }), + new EchartsBulletChartPlugin().configure({ key: VizType.Bullet }), new CalendarChartPlugin().configure({ key: VizType.Calendar }), new ChordChartPlugin().configure({ key: VizType.Chord }), new CountryMapChartPlugin().configure({ key: VizType.CountryMap }), From a33ab097e1e0b6f21737f507c387900c7ccbd69c Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 20 Jul 2026 19:30:03 -0700 Subject: [PATCH 58/84] fix(time-table): escape backslashes in flattened labels; actionable null-query-context errors Escaping only commas let group tuples like (a\\, b) and (a-comma, b) flatten to identical column keys; backslashes are now escaped first, with a regression test pinning the collision pair. The cache warm-up error for charts whose query context has not been generated yet tells the operator how to resolve it, and UPDATING.md documents the one-time lazy generation for charts migrated in place. Co-Authored-By: Claude Opus 4.8 (1M context) --- UPDATING.md | 1 + .../config/transformProps/transformData.test.ts | 15 +++++++++++++++ .../config/transformProps/transformData.ts | 7 +++++-- superset/commands/chart/warm_up_cache.py | 5 ++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index cf80f076964c..23b9d0a0c0e3 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -195,6 +195,7 @@ in a later major version. - [41044](https://github.com/apache/superset/issues/41044): Removes the deprecated `AVOID_COLORS_COLLISION` feature flag (it defaulted to `True`). Color-collision avoidance is now permanently enabled; any config override setting it to `False` is ignored. - [41714](https://github.com/apache/superset/pull/41714): **Breaking — the legacy `explore_json` chart-data pipeline is removed** at its long-declared `5.0.0` EOL. The `/superset/explore_json/` and `/superset/explore_json/data/` endpoints, `superset/viz.py`, the `Slice.viz` property, the `get_viz` factory, the `load_explore_json_into_cache` celery task and the `viz=` overload of `security_manager.raise_for_access` are gone. Anything importing `superset.viz` must migrate to the QueryContext / `pandas_postprocessing` pipeline behind `/api/v1/chart/data`. All 15 remaining legacy charts were migrated first: most keep their `viz_type` and renderer (no action needed for saved charts), while saved nvd3 Bubble charts are auto-migrated to the ECharts Bubble Chart (`bubble_v2`) and saved "Time-series Percent Change" (`compare`) charts to the ECharts Line Chart — the nvd3 renderer's interactive percent re-basing is not preserved. The deck.gl Multiple Layers chart now fetches its layers entirely client-side; its initial autozoom falls back to the saved viewport, and dashboard filter badges no longer aggregate child-layer filter metadata. +- [41714](https://github.com/apache/superset/pull/41714): Charts migrated in place keep a `NULL` saved query context until they are next opened in Explore (which regenerates it automatically) or re-saved. Until then, cache warm-up and annotation layers referencing such a chart report an actionable error rather than warming/rendering; opening the chart once resolves it. - [41714](https://github.com/apache/superset/pull/41714): **Breaking for third-party viz plugins** — the `useLegacyApi` field of `ChartMetadata` in `@superset-ui/core` is removed. Plugins that set it must provide a `buildQuery` and consume `/api/v1/chart/data`. The migrated first-party packages also drop their `legacy-` prefix: `@superset-ui/legacy-plugin-chart-{calendar,chord,country-map,horizon,paired-t-test,parallel-coordinates,partition,rose,world-map}` → `@superset-ui/plugin-chart-*`, and `@superset-ui/legacy-preset-chart-nvd3` → `@superset-ui/preset-chart-nvd3`. The `can_explore_json` permission is no longer created or granted; custom roles referencing it should switch to the `can_read` permissions on `Chart`. - [41813](https://github.com/apache/superset/pull/41813): `redis` (the Python client, `redis-py`) is bumped from 5.3.1 to 8.0.1. redis-py 8 changes several connection defaults; Superset's own Redis-backed features (`GLOBAL_ASYNC_QUERIES_CACHE_BACKEND`, `DISTRIBUTED_COORDINATION_CONFIG`, and the MCP Redis store) explicitly pin the pre-upgrade behavior so this bump is a no-op for them: the wire protocol stays RESP2 (not the new RESP3 default, which requires Redis/Sentinel 6+ to speak `HELLO`) and there is still no socket timeout by default (redis-py 8 defaults to 5s, which could otherwise newly time out large cached payloads or slow networks). The no-timeout default can now be overridden via two new config keys, `CACHE_REDIS_SOCKET_TIMEOUT` / `CACHE_REDIS_SOCKET_CONNECT_TIMEOUT`, on any `CacheConfig` dict using `CACHE_TYPE: RedisCache` or `RedisSentinelCache`. Separately, redis-py 6+ changed the default for `ssl_check_hostname` from `False` to `True` for SSL connections using `ssl_cert_reqs="required"` (the default) — this is a security improvement, so it has **not** been reverted; deployments with `CACHE_REDIS_SSL=True` whose certificates lack a hostname matching the connection address should set `CACHE_REDIS_SSL_CERT_REQS="none"` (disables cert verification entirely, matching hostname-check bypass) or replace the certificate. General-purpose cache/results backends configured via `CACHE_CONFIG` / `DATA_CACHE_CONFIG` / `RESULTS_BACKEND` with `CACHE_TYPE: RedisCache` go through `flask-caching`'s own Redis backend (outside Superset's code) and are subject to the same new defaults; pass `socket_timeout` / `protocol` via `CACHE_OPTIONS` there if needed. Celery broker and result-backend connections (built by `kombu`, also outside Superset's code) keep their no-socket-timeout behavior (`kombu` passes `socket_timeout=None` explicitly) but do **not** pin the wire protocol, so they follow redis-py's RESP3 default — which requires a Redis server new enough to speak `HELLO` (Redis 6+). Deployments using a pre-6.0 Redis server (EOL) as a Celery broker should upgrade the server before taking this bump. diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts index a01a78c51fa3..c12174c3c650 100644 --- a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts +++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.test.ts @@ -93,3 +93,18 @@ test('preserves sub-second timestamps as distinct keys', () => { '2024-01-01 00:00:00.123000', ]); }); + +test('escapes backslashes so escaped commas cannot collide with literal ones', () => { + // Under comma-only escaping, group tuples ("a\\", "b") and ("a,", "b") + // both flattened to "a\\, b"; escaping backslashes first keeps them apart. + const { columns } = transformData( + [ + { __timestamp: 0, g1: 'a\\', g2: 'b', sum__num: 1 }, + { __timestamp: 0, g1: 'a,', g2: 'b', sum__num: 2 }, + ], + ['g1', 'g2'], + ['sum__num'], + ); + expect(columns).toHaveLength(2); + expect(new Set(columns).size).toBe(2); +}); diff --git a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts index 2e06d43b04a2..ac5ef12027b6 100644 --- a/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts +++ b/superset-frontend/src/visualizations/TimeTable/config/transformProps/transformData.ts @@ -20,8 +20,11 @@ import { DTTM_ALIAS } from '@superset-ui/core'; const FLAT_COLUMN_SEPARATOR = ', '; -// escape every bare comma, exactly like the backend escape_separator -const escapeSeparator = (value: string) => value.replace(/,/g, '\\,'); +// Escape backslashes before commas so values like "a\\," and "a," cannot +// collide once joined (the backend escape_separator shares this gap, but +// these labels are only ever built client-side). +const escapeSeparator = (value: string) => + value.replace(/\\/g, '\\\\').replace(/,/g, '\\,'); const pad = (part: number) => String(part).padStart(2, '0'); diff --git a/superset/commands/chart/warm_up_cache.py b/superset/commands/chart/warm_up_cache.py index 11ff21683ad6..2f9e7d78cc2c 100644 --- a/superset/commands/chart/warm_up_cache.py +++ b/superset/commands/chart/warm_up_cache.py @@ -60,7 +60,10 @@ def _warm_up_non_legacy_cache(self, chart: Slice) -> tuple[Any, Any]: query_context = chart.get_query_context() if not query_context: - raise ChartInvalidError("Chart's query context does not exist") + raise ChartInvalidError( + "Chart's query context does not exist. Open the chart in " + "Explore once (or re-save it) to generate it." + ) # Apply dashboard filters if dashboard_id is provided if dashboard_filters := self._get_dashboard_filters(chart.id): From 52b5110a533e6e05a660854a26e651e927617f4a Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 20 Jul 2026 19:50:37 -0700 Subject: [PATCH 59/84] style: prettier formatting for UPDATING.md and SavedQueries test Co-Authored-By: Claude Opus 4.8 (1M context) --- UPDATING.md | 82 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index 23b9d0a0c0e3..387cf80504fc 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -216,7 +216,7 @@ in a later major version. - **`SqlaTable.sql_url` query-string format.** `SqlaTable.sql_url` now URL-encodes `table_name` and joins it as a query parameter rather than concatenating a second `?`. Previously, with `Database.sql_url` returning `/sqllab/?dbid=`, the concatenation produced `/sqllab/?dbid=?table_name=` — a malformed second `?` that broke the query parser. External code that parsed the legacy `?table_name=` shape now sees properly percent-encoded values (e.g. `/` → `%2F`, ` ` → `+` or `%20`); decode with `urllib.parse.parse_qsl`. -- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` *and* who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host. +- **New config flag `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE` (default `False`).** Share/permalink URLs now substitute `window.location.origin` for the backend-supplied origin so a proxied or subdirectory-deployed Superset never hands the user an unreachable internal hostname. Operators whose reverse proxy correctly forwards `X-Forwarded-Host` _and_ who want permalinks to carry the backend's literal origin can opt out by setting `EMBEDDED_DISABLE_PERMALINK_ORIGIN_REWRITE = True` in `superset_config.py`. Default `False` (rewrite is on); flipping the default would regress the dominant proxied/subdir deployment to an unreachable host. - [41651](https://github.com/apache/superset/pull/41651): **New do-not-translate standard for translation catalogs.** Strings that must stay identical to the source — icon names (e.g. `bolt`), enum/option values (`step-after`), SQL keywords, API field names (`error_message`), code constants, and example placeholders — are now marked with a `#. do-not-translate` extracted comment. The list lives in the `superset/translations/do-not-translate.txt` registry; `scripts/translations/apply_do_not_translate.py` stamps the marker onto `messages.pot` during `babel_update.sh`, and `pybabel update` propagates it to every `.po`, so the status is consistent across all languages. The AI backfill (`backfill_po.py`) and translators leave these entries untranslated (source fallback). The legacy per-catalog convention (a `# Не переводить` translator comment in the `ru` catalog) is still honored for back-compat but is superseded by this standard; contributors adding new machine-read strings should add the msgid to the registry rather than annotating individual catalogs. @@ -253,7 +253,7 @@ Theme tokens are unaffected — antd 6 removed none of the tokens Superset expos ### Guest-token RLS rules reject unknown fields -The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a *global* rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected. +The `rls` rules passed to `POST /api/v1/security/guest_token/` are now validated strictly: a rule may only contain `dataset` and `clause`. Previously unknown fields were silently dropped, so a mistyped or legacy scope key (most commonly `datasource` instead of `dataset`) produced a rule with no `dataset`, which is treated as a _global_ rule applied to every dataset the embedded resource can reach. Such a request now returns HTTP 400 identifying the offending field instead of issuing a token with an unintended global rule. Integrators that were sending extra fields in RLS rules must remove them; valid dataset-scoped (`{"dataset": 41, "clause": "..."}`) and global (`{"clause": "..."}`) rules are unaffected. ### MCP service requires `MCP_JWT_AUDIENCE` when JWT auth is enabled @@ -337,6 +337,7 @@ ALTER TABLE tagged_object DROP CONSTRAINT ; -- MySQL: find names via `SHOW CREATE TABLE tagged_object;` ALTER TABLE tagged_object DROP FOREIGN KEY ; ``` + ### Entity version-history infrastructure (gated off by default) Introduces the schema and SQLAlchemy-Continuum wiring that captures version history for charts, dashboards, and datasets, plus read-only `GET /api/v1/{chart,dashboard,dataset}//versions/` endpoints. This ships **inert**: a new config flag `ENABLE_VERSIONING_CAPTURE` defaults to `False`, so no save writes any version rows and the endpoints return empty. It is an operational kill-switch (a release toggle that becomes a permanent ops switch), not a feature flag — set it to `True` to enable capture once validated. The migration is additive; existing entity `PUT` responses gain `old_version_uuid` / `new_version_uuid` body fields and an `ETag` header (both null/absent when capture is off). @@ -353,12 +354,12 @@ These are behavior changes that take effect on upgrade regardless of `ENABLE_VER A read-only companion to the version-history endpoints: each entity type gains a `GET /api/v1/{chart,dashboard,dataset}//activity/` endpoint returning a chronological, access-filtered stream of edits — the entity's own edits plus, for charts and dashboards, transitive edits to related entities during their association windows. Datasets have no related layer in V2, so `include=related` returns an empty stream for a dataset and `include=all` reduces to the dataset's own edits. -| Param | Type | Default | Purpose | -|---|---|---|---| -| `since` / `until` | ISO 8601 | — | Bound `issued_at` | -| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both | -| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) | -| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) | +| Param | Type | Default | Purpose | +| -------------------- | ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------ | +| `since` / `until` | ISO 8601 | — | Bound `issued_at` | +| `include` | `self` \| `related` \| `all` | `all` | Own edits, related edits, or both | +| `q` | string | — | Case-insensitive search over the full history, applied before pagination (so `count` reflects matches) | +| `page` / `page_size` | integer | `0` / `25` | Pagination (`page_size` clamped to 200) | Authorization reuses the resource's `can_read` permission and per-object `raise_for_access`; related-entity rows are visibility-filtered to what the caller may see. The stream is empty unless version capture is on (`ENABLE_VERSIONING_CAPTURE`). @@ -409,6 +410,7 @@ Operators can tune or disable the policy via config: ### Data uploads bounded by UPLOAD_MAX_FILE_SIZE_BYTES Single data-file uploads (CSV, Excel, columnar) are now bounded by the `UPLOAD_MAX_FILE_SIZE_BYTES` config option, which defaults to `100 * 1024 * 1024` (100 MB). Files larger than this are rejected with a `413` before their contents are buffered into memory. Set `UPLOAD_MAX_FILE_SIZE_BYTES = None` to disable the check and restore unbounded uploads. + ### Currency symbol position follows the locale when unset When a chart's currency control leaves the **Prefix or suffix** field empty, the currency symbol position is now derived from the deployment locale's own convention via `Intl.NumberFormat` instead of always defaulting to a suffix. For example, under the default `en-US` locale `USD`, `GBP`, and `EUR` render as a prefix (`$ 1,000`), while eurozone locales such as `fr-FR` render `EUR` as a suffix (`1 000 €`). An explicit Prefix/Suffix selection is always honored and is unaffected. @@ -534,7 +536,7 @@ SQLALCHEMY_ENCRYPTED_FIELD_ENGINE = "aes" ```bash superset re-encrypt-secrets --engine aes-gcm ``` - A live instance keeps writing *new* secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped). + A live instance keeps writing _new_ secrets as AES-CBC during the window between step 2 and the restart in step 4; this second pass sweeps those up (it is idempotent, so already-migrated values are skipped). Schedule the cutover in a quiet window. Runtime reads use only the single configured engine, so in a multi-worker deployment there is an unavoidable brief decrypt-outage between the migration commit and the last worker restarting with the new config — each migrator run is transactional, but the fleet-wide cutover is not zero-downtime. @@ -562,11 +564,11 @@ With the flag enabled: `DELETE /api/v1/dataset/` no longer hard-deletes the **Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_tables_deleted_at`) to the `tables` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `tables` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). Production deployments with many thousands of datasets should run this migration during a maintenance window. -**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback. +**Rollback note:** if the application code is rolled back after datasets have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted datasets silently become live, active datasets with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback. **SQL Lab / dataset-creation flows:** creating a dataset over a table whose dataset sits in the trash is refused. The SQL Lab "save as dataset" flow (`get_or_create_dataset`) and file uploads return a **422 naming the hidden twin and the restore endpoint**; the plain create, update, and duplicate paths currently fail with the generic "already exists" 422. In all cases the remediation is the same: restore the hidden dataset (or use a different table name). Perm-string maintenance also covers hidden rows: renaming a database rewrites `perm`/`schema_perm`/`catalog_perm` on soft-deleted datasets and their charts, so a later restore does not resurrect stale permission strings. -**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. +**Importer behavior:** importing a dataset YAML whose UUID matches an existing **soft-deleted** dataset is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dataset imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dataset's exact UUID is an explicit request to bring it back. The restore preserves the original PK, the chart back-reference, `table_columns`, and `sql_metrics`. Non-editors get `ImportFailedError`. Callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. **Uniqueness-validation changes that apply regardless of the feature flag:** two dataset uniqueness checks were tightened alongside this work and are active even with `SOFT_DELETE` off. (1) Create/update uniqueness treats a dataset whose `catalog` is `NULL` as belonging to the database's default catalog, so a legacy twin pair (`catalog=NULL` vs. `catalog=`, same database/schema/name) that older versions allowed now fails validation with "already exists" when either row is edited — resolve by renaming or removing one of the twins. (2) Duplicating a dataset now checks name collisions scoped to the target (database, catalog, schema) instead of globally by name alone: duplicates into other databases that were previously blocked are now allowed. @@ -586,9 +588,9 @@ With the flag enabled: `DELETE /api/v1/chart/` no longer hard-deletes the ch **Schema migration:** the migration adds a nullable `deleted_at` column and an index on it (`ix_slices_deleted_at`) to the `slices` table. The column add is instant; the index build runs inline (no `CONCURRENTLY`) and may briefly block writes on the `slices` table (INSERT/UPDATE/DELETE are queued while the index builds; reads are unaffected) on large Postgres deployments. MySQL InnoDB builds the index online (no blocking). -**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) *before* downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback. +**Rollback note:** if the application code is rolled back after charts have been soft-deleted, the older code path's visibility filter no longer applies and previously hidden rows become visible to the older code. Pair the rollback with a data decision (restore the rows, hard-delete them, or also downgrade the migration) rather than assuming the old hard-delete semantics still hold. **Downgrading the migration destroys the deletion markers**: `downgrade()` drops the `deleted_at` column, so any not-yet-restored soft-deleted charts silently become live, active charts with no record they were ever deleted. Reconcile the trash (restore or hard-delete each row) _before_ downgrading, and disable the `SOFT_DELETE` flag first so no new soft deletes land mid-rollback. -**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. +**Importer behavior:** importing a chart YAML whose UUID matches an existing **soft-deleted** chart is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active chart imported without `overwrite=true` is returned unchanged, but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted chart's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all out-of-archive references (`dashboard_slices` junctions, `report.chart_id`, tag rows). The operation is permission-gated: non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. - [39914](https://github.com/apache/superset/pull/39914) `ALERT_REPORT_SLACK_V2` now defaults to `True` and the legacy Slack v1 integration (`Slack` recipient type, `files.upload` API) is deprecated for removal in the next major. Slack blocked new apps from `files.upload` in May 2024 and fully retired the method for all apps on November 12, 2025; because the v1 path sends files through `files.upload`, v1 file-bearing sends now fail at the API level — only text-only `chat_postMessage` still works via the legacy path. Grant your Slack bot the `channels:read` and `groups:read` scopes so existing `Slack` recipients can be auto-upgraded to `SlackV2` on next send. Operators who explicitly override the flag to `False`, or whose Slack bot is missing those scopes, will see deprecation warnings while text-only sends continue through the legacy path. @@ -612,7 +614,7 @@ The partial-index replacement is dialect-dependent: PostgreSQL uses a native `WH **Slug semantics:** on PostgreSQL and MySQL 8.0.13+, the slug of a soft-deleted dashboard is **free for reuse**. A new active dashboard can claim it immediately. Restoring a soft-deleted dashboard whose slug has since been claimed returns **422 with a clean error** (`DashboardSlugConflictError`) — rename one of the dashboards and retry; the restore is not silently rejected by a database-level constraint violation. -**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored *and* has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. +**Importer behavior:** importing a dashboard YAML whose UUID matches an existing **soft-deleted** dashboard is treated as an implicit restore-with-update — **and this happens even when `overwrite` is not set**. This is a deliberate asymmetry with active rows: an active dashboard imported without `overwrite=true` is returned unchanged (the import never mutates it), but a soft-deleted UUID match is restored _and_ has the upload's contents applied regardless of the `overwrite` argument, on the reasoning that re-importing a deleted dashboard's exact UUID is an explicit request to bring it back. The restore preserves the original PK and all pre-deletion relationship rows (`dashboard_slices` junctions, editor/viewer subjects, tags). Callers whose imports must never mutate existing state should treat bundles that may contain previously deleted UUIDs accordingly. The operation is permission-gated: it requires `can_write` and editorship of the deleted row (or admin) — non-editors get `ImportFailedError`, and callers without `can_write` get `ImportFailedError` instead of silently receiving the soft-deleted row. ### Engine spec capability flag: `supports_offset` @@ -624,10 +626,10 @@ A new `BaseEngineSpec.supports_offset` attribute (default `True`) indicates whet A new feature flag `GRANULAR_EXPORT_CONTROLS` introduces three fine-grained permissions that replace the legacy `can_csv` permission: -| Permission | Controls | -|---|---| -| `can_export_data` | CSV, Excel, JSON exports | -| `can_export_image` | Screenshot/PDF exports | +| Permission | Controls | +| -------------------- | ---------------------------- | +| `can_export_data` | CSV, Excel, JSON exports | +| `can_export_image` | Screenshot/PDF exports | | `can_copy_clipboard` | Copy-to-clipboard operations | When the feature flag is enabled, these permissions are enforced on both the frontend (disabled buttons with tooltips) and backend (403 responses from API endpoints). When disabled, legacy `can_csv` behavior is preserved. @@ -665,14 +667,17 @@ The Kenya country map has been updated to reflect the 47 counties established un MCP (Model Context Protocol) tools now include enhanced observability instrumentation for monitoring and debugging: **Two-layer instrumentation:** + 1. **Middleware layer** (`LoggingMiddleware`): Automatically logs all MCP tool calls with `duration_ms` and `success` status in the audit log (Action Log UI, logs table) 2. **Sub-operation tracking**: All 19 MCP tools include granular `event_logger.log_context()` blocks for tracking individual operations like validation, database writes, and query execution **Action naming convention:** + - Tool-level logs: `mcp_tool_call` (via middleware) - Sub-operation logs: `mcp.{tool_name}.{operation}` (e.g., `mcp.generate_chart.validation`, `mcp.execute_sql.query_execution`) **Querying MCP logs:** + ```sql -- Top slowest MCP operations SELECT action, COUNT(*) as calls, AVG(duration_ms) as avg_ms @@ -707,6 +712,7 @@ A new `DISTRIBUTED_COORDINATION_CONFIG` configuration provides a unified Redis-b The distributed coordination is used by the Global Task Framework (GTF) for abort notifications and task completion signaling, and will eventually replace `GLOBAL_ASYNC_QUERIES_CACHE_BACKEND` as the standard signaling backend. Configuring this is recommended for Redis enabled production deployments. Example configuration in `superset_config.py`: + ```python DISTRIBUTED_COORDINATION_CONFIG = { "CACHE_TYPE": "RedisCache", @@ -721,9 +727,11 @@ See `superset/config.py` for complete configuration options. ### WebSocket config for GAQ with Docker [35896](https://github.com/apache/superset/pull/35896) and [37624](https://github.com/apache/superset/pull/37624) updated documentation on how to run and configure Superset with Docker. Specifically for the WebSocket configuration, a new `docker/superset-websocket/config.example.json` was added to the repo, so that users could copy it to create a `docker/superset-websocket/config.json` file. The existing `docker/superset-websocket/config.json` was removed and git-ignored, so if you're using GAQ / WebSocket make sure to: + - Stash/backup your existing `config.json` file, to re-apply it after (will get git-ignored going forward) - Update the `volumes` configuration for the `superset-websocket` service in your `docker-compose.override.yml` file, to include the `docker/superset-websocket/config.json` file. For example: -``` yaml + +```yaml services: superset-websocket: volumes: @@ -736,7 +744,9 @@ services: ### Example Data Loading Improvements #### New Directory Structure + Examples are now organized by name with data and configs co-located: + ``` superset/examples/ ├── _shared/ # Shared database & metadata configs @@ -749,12 +759,14 @@ superset/examples/ ``` #### Simplified Parquet-based Loading + - Auto-discovery: create `superset/examples/my_dataset/data.parquet` to add a new example - Parquet is an Apache project format: compressed (~27% smaller), self-describing schema - YAML configs define datasets, charts, and dashboards declaratively - Removed Python-based data generation from individual example files #### Test Data Reorganization + - Moved `big_data.py` to `superset/cli/test_loaders.py` - better reflects its purpose as a test utility - Fixed inverted logic for `--load-test-data` flag (now correctly includes .test.yaml files when flag is set) - Clarified CLI flags: @@ -764,6 +776,7 @@ superset/examples/ - `--load-big-data` / `-b`: Generate synthetic stress-test data #### Bug Fixes + - Fixed numpy array serialization for PostgreSQL (converts complex types to JSON strings) - Fixed KeyError for `allow_csv_upload` field in database configs (now optional with default) - Fixed test data loading logic that was incorrectly filtering files @@ -773,6 +786,7 @@ superset/examples/ The MCP (Model Context Protocol) service enables AI assistants and automation tools to interact programmatically with Superset. #### New Features + - MCP service infrastructure with FastMCP framework - Tools for dashboards, charts, datasets, SQL Lab, and instance metadata - Optional dependency: install with `pip install apache-superset[fastmcp]` @@ -782,6 +796,7 @@ The MCP (Model Context Protocol) service enables AI assistants and automation to #### New Configuration Options **Development** (single-user, local testing): + ```python # superset_config.py MCP_DEV_USERNAME = "admin" # User for MCP authentication @@ -790,6 +805,7 @@ MCP_SERVICE_PORT = 5008 ``` **Production** (JWT-based, multi-user): + ```python # superset_config.py MCP_AUTH_ENABLED = True @@ -835,12 +851,14 @@ superset mcp run --port 5008 --use-factory-config The MCP service runs as a **separate process** from the Superset web server. **Important**: + - Requires same Python environment and configuration as Superset - Shares database connections with main Superset app - Can be scaled independently from web server - Requires `fastmcp` package (optional dependency) **Installation**: + ```bash # Install with MCP support pip install apache-superset[fastmcp] @@ -854,6 +872,7 @@ Use systemd, supervisord, or Kubernetes to manage the MCP service process. See `superset/mcp_service/PRODUCTION.md` for deployment guides. **Security**: + - Development: Uses `MCP_DEV_USERNAME` for single-user access - Production: **MUST** configure JWT authentication - See `superset/mcp_service/SECURITY.md` for details @@ -873,8 +892,10 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides. - [35062](https://github.com/apache/superset/pull/35062): Changed the function signature of `setupExtensions` to `setupCodeOverrides` with options as arguments. ### Breaking Changes + - [37370](https://github.com/apache/superset/pull/37370): The `APP_NAME` configuration variable no longer controls the browser window/tab title or other frontend branding. Application names should now be configured using the theme system with the `brandAppName` token. The `APP_NAME` config is still used for backend contexts (MCP service, logs, etc.) and serves as a fallback if `brandAppName` is not set. - **Migration:** + ```python # Before (Superset 5.x) APP_NAME = "My Custom App" @@ -918,16 +939,16 @@ See `superset/mcp_service/PRODUCTION.md` for deployment guides. Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a composite `PRIMARY KEY (fk1, fk2)` on their two foreign-key columns. The surrogate `id` is dropped, and the redundant `UNIQUE (fk1, fk2)` on the two tables that carried one is removed (now subsumed by the PK). -| Table | Composite PK | -|---|---| -| `dashboard_roles` | `(dashboard_id, role_id)` | -| `dashboard_slices` | `(dashboard_id, slice_id)` | -| `dashboard_user` | `(user_id, dashboard_id)` | +| Table | Composite PK | +| ---------------------- | ------------------------------- | +| `dashboard_roles` | `(dashboard_id, role_id)` | +| `dashboard_slices` | `(dashboard_id, slice_id)` | +| `dashboard_user` | `(user_id, dashboard_id)` | | `report_schedule_user` | `(user_id, report_schedule_id)` | -| `rls_filter_roles` | `(role_id, rls_filter_id)` | -| `rls_filter_tables` | `(table_id, rls_filter_id)` | -| `slice_user` | `(user_id, slice_id)` | -| `sqlatable_user` | `(user_id, table_id)` | +| `rls_filter_roles` | `(role_id, rls_filter_id)` | +| `rls_filter_tables` | `(table_id, rls_filter_id)` | +| `slice_user` | `(user_id, slice_id)` | +| `sqlatable_user` | `(user_id, table_id)` | **Before upgrading:** @@ -938,6 +959,7 @@ Eight M:N association tables move from a synthetic `id INTEGER PRIMARY KEY` to a For large `dashboard_slices` / `report_schedule_user` tables, see the operator runbook in [#39859](https://github.com/apache/superset/pull/39859) — pre-flight inventory queries, per-dialect lock-window sizing, and the duplicate / NULL-FK roll-up — to plan the maintenance window. ## 6.0.0 + - [33055](https://github.com/apache/superset/pull/33055): Upgrades Flask-AppBuilder to 5.0.0. The AUTH_OID authentication type has been deprecated and is no longer available as an option in Flask-AppBuilder. OpenID (OID) is considered a deprecated authentication protocol - if you are using AUTH_OID, you will need to migrate to an alternative authentication method such as OAuth, LDAP, or database authentication before upgrading. - [34871](https://github.com/apache/superset/pull/34871): Fixed Jest test hanging issue from Ant Design v5 upgrade. MessageChannel is now mocked in test environment to prevent rc-overflow from causing Jest to hang. Test environment only - no production impact. - [34782](https://github.com/apache/superset/pull/34782): Dataset exports now include the dataset ID in their file name (similar to charts and dashboards). If managing assets as code, make sure to rename existing dataset YAMLs to include the ID (and avoid duplicated files). @@ -946,8 +968,8 @@ For large `dashboard_slices` / `report_schedule_user` tables, see the operator r - Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"` - Custom colors are no longer supported to maintain consistency with Ant Design components - [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards. -Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots. -`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0). + Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots. + `thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0). - [33084](https://github.com/apache/superset/pull/33084) The DISALLOWED_SQL_FUNCTIONS configuration now includes additional potentially sensitive database functions across PostgreSQL, MySQL, SQLite, MS SQL Server, and ClickHouse. Existing queries using these functions may now be blocked. Review your SQL Lab queries and dashboards if you encounter "disallowed function" errors after upgrading - [34235](https://github.com/apache/superset/pull/34235) CSV exports now use `utf-8-sig` encoding by default to include a UTF-8 BOM, improving compatibility with Excel. - [34258](https://github.com/apache/superset/pull/34258) changing the default in Dockerfile to INCLUDE_CHROMIUM="false" (from "true") in the past. This ensures the `lean` layer is lean by default, and people can opt-in to the `chromium` layer by setting the build arg `INCLUDE_CHROMIUM=true`. This is a breaking change for anyone using the `lean` layer, as it will no longer include Chromium by default. From c7f0167a28a45f3bad6b57c45623be46dcdc42c5 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 20 Jul 2026 23:03:08 -0700 Subject: [PATCH 60/84] test(charts): update warm-up cache assertions for the actionable message Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/integration_tests/charts/api_tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests/charts/api_tests.py b/tests/integration_tests/charts/api_tests.py index 727df9bfeb7a..1a131742008d 100644 --- a/tests/integration_tests/charts/api_tests.py +++ b/tests/integration_tests/charts/api_tests.py @@ -2201,7 +2201,8 @@ def test_warm_up_cache_no_query_context(self) -> None: "result": [ { "chart_id": slc.id, - "viz_error": "Chart's query context does not exist", + "viz_error": "Chart's query context does not exist. Open the " + "chart in Explore once (or re-save it) to generate it.", "viz_status": None, }, ], @@ -2228,7 +2229,8 @@ def test_warm_up_cache_no_datasource(self) -> None: "result": [ { "chart_id": slc.id, - "viz_error": "Chart's query context does not exist", + "viz_error": "Chart's query context does not exist. Open the " + "chart in Explore once (or re-save it) to generate it.", "viz_status": None, }, ], From 9fe31e3ec57ecab3b68c4d7438a8a9cf38097f3b Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 21 Jul 2026 09:15:39 -0700 Subject: [PATCH 61/84] test(embedded): store a query_context on the pivot collapse fixture chart The spec's fixture chart was created with params only. Real charts saved through Explore always persist a query_context, and guest (embedded) chart data requests are validated against the stored chart: with no stored query_context and no columns/groupby params keys, the pivot payload's query columns read as tampering and every request 403s. The suite previously passed only when the embedded iframe happened to ride the admin session from storageState instead of the guest token (a cookie race between the /embedded response's anonymous session and the SDK's csrf/guest-token fetches), which masked the 403 on master. Co-Authored-By: Claude Fable 5 --- .../embedded/pivot-collapse-state.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts b/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts index 06fbc244e8e5..bc886f6c10dc 100644 --- a/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts +++ b/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts @@ -182,12 +182,43 @@ test.describe('Embedded Pivot Table collapse state (#33406)', () => { row_limit: 1000, order_desc: true, }; + // Charts saved through Explore always persist a query_context alongside + // params. Store one here too: guest (embedded) requests are validated + // against the stored chart, and a params-only chart makes the guest + // payload look tampered (its query `columns` aren't found on the chart), + // failing every chart data request with a 403. + const queryContext = { + datasource: { id: datasetId, type: 'table' }, + force: false, + queries: [ + { + filters: [], + extras: { having: '', where: '' }, + applied_time_extras: {}, + columns: ['state', 'name'], + metrics: ['count'], + orderby: [['count', false]], + annotation_layers: [], + row_limit: 1000, + series_limit: 0, + order_desc: true, + url_params: {}, + custom_params: {}, + custom_form_data: {}, + }, + ], + form_data: params, + result_format: 'json', + result_type: 'full', + }; const chartResp = await apiPost(setupPage, 'api/v1/chart/', { slice_name: `pivot_collapse_repro_${Date.now()}`, viz_type: 'pivot_table_v2', datasource_id: datasetId, datasource_type: 'table', params: JSON.stringify(params), + query_context: JSON.stringify(queryContext), + query_context_generation: true, }); chartId = (await chartResp.json()).id; From 2261c6179151b832e1dca17cf4857ccb06950075 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 21 Jul 2026 11:14:42 -0700 Subject: [PATCH 62/84] docs(embedded): document the iframe session cookie race in the pivot spec Also retriggers CI so the merge ref picks up the master fix from #42274 (an empty commit does not trigger the path-gated workflows). Co-Authored-By: Claude Fable 5 --- .../tests/embedded/pivot-collapse-state.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts b/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts index bc886f6c10dc..0e386a53e03a 100644 --- a/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts +++ b/superset-frontend/playwright/tests/embedded/pivot-collapse-state.spec.ts @@ -38,6 +38,14 @@ * * NOTE: the embedded suite only runs when the embedded SDK bundle is built and * INCLUDE_EMBEDDED=true (CI sets both). It is skipped otherwise. + * + * NOTE: the embedded project runs with admin storageState, and the session + * cookie the iframe ends up with is racy: the /embedded/ response + * rotates it to an anonymous session while the SDK's parallel csrf and + * guest-token fetches rewrite the admin one, so chart data requests are + * evaluated as either admin or guest depending on which response lands last. + * Fixture charts must therefore hold up under GUEST evaluation — see the + * query_context note below — or the suite only passes when admin wins. */ import { test, expect, Browser, BrowserContext, Page } from '@playwright/test'; import { createServer, IncomingMessage, ServerResponse, Server } from 'http'; From b85468ee17aa892a1289313cc440c0da67971d3c Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 21 Jul 2026 11:57:46 -0700 Subject: [PATCH 63/84] chore: drop unused feature_flag_manager import after rebase Co-Authored-By: Claude Fable 5 --- superset/views/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset/views/utils.py b/superset/views/utils.py index bea2cff2c1cb..d58dcc57df00 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -41,7 +41,7 @@ SerializationError, SupersetException, ) -from superset.extensions import feature_flag_manager, security_manager +from superset.extensions import security_manager from superset.legacy import update_time_range from superset.models.core import Database from superset.models.dashboard import Dashboard From 1859b375a7b88af3883d120f6e4a1fd76012b020 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 22 Jul 2026 17:52:46 -0700 Subject: [PATCH 64/84] chore(migrations): re-id compare-chart migration and re-point bubble migration after rebase Master's add_extension_storage_table migration landed with revision id e5f6a7b8c9d0, colliding with the compare-chart migration's id; re-id it to 88360afb61ed and chain the bubble migration off master's head. Co-Authored-By: Claude Fable 5 --- ...7-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py | 4 ++-- ...02_21-30_88360afb61ed_migrate_compare_chart_to_echarts.py} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename superset/migrations/versions/{2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py => 2026-07-02_21-30_88360afb61ed_migrate_compare_chart_to_echarts.py} (97%) diff --git a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py index ce7d202065ba..ece219928587 100644 --- a/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-00_d4e5f6a7b8c9_migrate_bubble_chart_to_echarts.py @@ -17,7 +17,7 @@ """migrate_bubble_chart_to_echarts Revision ID: d4e5f6a7b8c9 -Revises: d24e6b0a9c7f +Revises: e5f6a7b8c9d0 Create Date: 2026-07-02 21:00:00.000000 """ @@ -31,7 +31,7 @@ # revision identifiers, used by Alembic. revision = "d4e5f6a7b8c9" -down_revision = "d24e6b0a9c7f" +down_revision = "e5f6a7b8c9d0" def upgrade(): diff --git a/superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py b/superset/migrations/versions/2026-07-02_21-30_88360afb61ed_migrate_compare_chart_to_echarts.py similarity index 97% rename from superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py rename to superset/migrations/versions/2026-07-02_21-30_88360afb61ed_migrate_compare_chart_to_echarts.py index 35766d84dbcc..fee717493945 100644 --- a/superset/migrations/versions/2026-07-02_21-30_e5f6a7b8c9d0_migrate_compare_chart_to_echarts.py +++ b/superset/migrations/versions/2026-07-02_21-30_88360afb61ed_migrate_compare_chart_to_echarts.py @@ -16,7 +16,7 @@ # under the License. """migrate_compare_chart_to_echarts -Revision ID: e5f6a7b8c9d0 +Revision ID: 88360afb61ed Revises: d4e5f6a7b8c9 Create Date: 2026-07-02 21:30:00.000000 @@ -30,7 +30,7 @@ from superset.migrations.shared.migrate_viz import MigrateCompareChart # revision identifiers, used by Alembic. -revision = "e5f6a7b8c9d0" +revision = "88360afb61ed" down_revision = "d4e5f6a7b8c9" From b73f6a8bd55c79d0639ff4d1f3deffca8447fe78 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 22 Jul 2026 18:56:12 -0700 Subject: [PATCH 65/84] fix(bullet): range labels inside their bands, always-on tooltip, legend-aware grid Range labels move from mid-bar scatter points onto the markArea bands (insideTopRight), so each label sits inside the range it names and the rightmost label no longer clips at the chart edge. The tooltip stays enabled regardless of the Show labels/legend toggles and the measure bar tooltip names the range the value falls within. The grid top now estimates wrapped legend rows from item text widths instead of assuming a single row. Co-Authored-By: Claude Fable 5 --- .../src/Bullet/transformProps.ts | 91 ++++++++++++++----- .../test/Bullet/transformProps.test.ts | 72 +++++++++++++++ 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts index 859f09bfe4cd..93b660089a2b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Bullet/transformProps.ts @@ -24,6 +24,7 @@ import { JsonObject, sanitizeHtml, } from '@superset-ui/core'; +import { t } from '@apache-superset/core/translation'; import type { EChartsCoreOption } from 'echarts/core'; import { Refs } from '../types'; import { EchartsBulletChartProps, BulletChartTransformedProps } from './types'; @@ -101,45 +102,99 @@ export default function transformProps( .map((value, i) => ({ value, label: rangeLabels[i] })) .filter(({ value }) => value > axisMin) .sort((a, b) => b.value - a.value); + // Each band's label sits inside its own top-right corner: the visible strip + // of a nested band ends at its threshold, so the label lands inside the + // range it names and the rightmost label cannot clip at the chart edge. const bands = sortedRanges.map(({ value, label }, i) => ({ name: label, itemStyle: { color: bandFills[Math.min(i, bandFills.length - 1)], }, - label: { show: false }, + label: { + show: Boolean(showLabels) && Boolean(label), + position: 'insideTopRight', + distance: theme.sizeUnit, + color: theme.colorTextSecondary, + formatter: () => label ?? '', + }, // markArea item: [{start}, {end}] coords: [{ xAxis: axisMin }, { xAxis: value }], })); + // Which labeled range a measure falls within, for the bar tooltip. Range + // labels render at band edges, so naming the containing band on hover + // disambiguates which side of a boundary a value sits on. + const ascendingRanges = [...ranges] + .map((value, i) => ({ value, label: rangeLabels[i] })) + .sort((a, b) => a.value - b.value); + const containingRangeLabel = (value: number): string | undefined => { + const hit = ascendingRanges.find(range => value <= range.value); + if (hit?.label) return hit.label; + const last = ascendingRanges[ascendingRanges.length - 1]; + if (!hit && last?.label) return `> ${last.label}`; + return undefined; + }; + // Resolve by position rather than `indexOf(value)` so duplicate marker or // marker-line values each keep their own label instead of collapsing onto // the first match. const markerLabelAt = (index: number, labels: string[], values: number[]) => labels[index] ? labels[index] : formatter(values[index]); + const rangeName = (value: number, i: number) => + rangeLabels[i] + ? `${rangeLabels[i]}: \u2264 ${formatter(value)}` + : `\u2264 ${formatter(value)}`; + const markerName = (value: number, i: number, labels: string[]) => + labels[i] ? `${labels[i]}: ${formatter(value)}` : String(formatter(value)); + + // ECharts wraps a top legend onto extra rows but does not push the grid + // down to make room, so estimate the wrapped row count from item text + // widths and reserve grid space for it. + const legendNames = [ + metricLabel, + ...ranges.map(rangeName), + ...markers.map((value, i) => markerName(value, i, markerLabels)), + ...markerLines.map((value, i) => markerName(value, i, markerLineLabels)), + ]; + const estimateLegendRows = (names: string[]): number => { + // legend icon width + icon-text gap + inter-item gap (ECharts defaults) + const itemOverhead = 25 + 5 + 10; + const avgCharWidth = theme.fontSize * 0.6; + const available = Math.max(width - theme.sizeUnit * 4, 1); + let rows = 1; + let cursor = 0; + names.forEach(name => { + const itemWidth = itemOverhead + name.length * avgCharWidth; + if (cursor > 0 && cursor + itemWidth > available) { + rows += 1; + cursor = 0; + } + cursor += itemWidth; + }); + return rows; + }; + const legendRowHeight = theme.fontSize + theme.sizeUnit * 3; + const gridTop = showLegend + ? estimateLegendRows(legendNames) * legendRowHeight + theme.sizeUnit * 2 + : theme.sizeUnit * 2; + // The measure bar spans MEASURE_BAR_FRACTION of the category band // (roughly the grid height), so a percentage symbolOffset lands inside a // tall bar. Compute pixels: half the bar plus a small gap below it. - const gridHeight = Math.max(height - theme.sizeUnit * 10, 40); + const gridHeight = Math.max(height - gridTop - theme.sizeUnit * 6, 40); const rowHeight = gridHeight / categories.length; const markerOffsetPx = Math.round( (MEASURE_BAR_FRACTION / 2) * rowHeight + 12, ); - const rangeName = (value: number, i: number) => - rangeLabels[i] - ? `${rangeLabels[i]}: \u2264 ${formatter(value)}` - : `\u2264 ${formatter(value)}`; - const markerName = (value: number, i: number, labels: string[]) => - labels[i] ? `${labels[i]}: ${formatter(value)}` : String(formatter(value)); - const echartOptions: EChartsCoreOption = { animation: false, // Optional legend listing every range, marker and line as a toggleable // named entry; each is its own series so ECharts handles the toggling. legend: { show: Boolean(showLegend), top: 0 }, grid: { - top: showLegend ? theme.sizeUnit * 10 : theme.sizeUnit * 2, + top: gridTop, bottom: theme.sizeUnit * 6, left: theme.sizeUnit * 2, right: theme.sizeUnit * 2, @@ -162,9 +217,10 @@ export default function transformProps( axisTick: { show: false }, axisLabel: { show: grouped, color: theme.colorTextSecondary }, }, + // The tooltip stays on regardless of the label and legend toggles: it is + // the only place hover reveals which range a value falls within. tooltip: { confine: true, - show: !showLabels, }, series: [ { @@ -177,8 +233,10 @@ export default function transformProps( formatter: (params?: { dataIndex?: number }) => { const i = params?.dataIndex ?? 0; const prefix = grouped ? `${categories[i]} \u2014 ` : ''; + const band = containingRangeLabel(measures[i]); + const bandLine = band ? `
${t('Range')}: ${band}` : ''; return sanitizeHtml( - `${prefix}${metricLabel}: ${formatter(measures[i])}`, + `${prefix}${metricLabel}: ${formatter(measures[i])}${bandLine}`, ); }, }, @@ -210,15 +268,6 @@ export default function transformProps( `${rangeLabels[index] || ''} \u2264 ${formatter(value)}`, ), }, - label: { - show: - Boolean(showLabels) && - Boolean(rangeLabels[index]) && - row === categories.length - 1, - position: 'top', - color: theme.colorTextSecondary, - formatter: () => String(rangeLabels[index] || ''), - }, })), symbol: 'rect', symbolSize: [14, 40], diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts index b5828ac7da98..306db08e5271 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Bullet/transformProps.test.ts @@ -121,6 +121,78 @@ test('keeps distinct labels for duplicate marker and marker-line values', () => expect(markerTooltips[1]).toContain('forecast'); }); +test('keeps the tooltip enabled regardless of the label and legend toggles', () => { + const { echartOptions } = transformProps( + chartProps({ showLabels: true, showLegend: true }), + ); + expect((echartOptions as any).tooltip.show).not.toBe(false); +}); + +test('names the containing range in the measure bar tooltip', () => { + const { echartOptions } = transformProps(chartProps()); + // measure 120 falls within the 100..200 band labelled "mid" + expect((echartOptions as any).series[0].tooltip.formatter()).toContain('mid'); +}); + +test('marks a measure beyond every range as past the last label', () => { + const props = new ChartProps({ + width: 800, + height: 200, + formData, + theme: supersetTheme, + queriesData: [{ data: [{ sum__num: 450 }] }], + hooks: {}, + }) as unknown as EchartsBulletChartProps; + const { echartOptions } = transformProps(props); + expect((echartOptions as any).series[0].tooltip.formatter()).toContain( + '> high', + ); +}); + +test('renders range labels inside the top-right corner of their bands', () => { + const { echartOptions } = transformProps(chartProps({ showLabels: true })); + const { series } = echartOptions as any; + + // band labels live on the markArea items, not on the hover-target points + const bandLabels = series[0].markArea.data.map((d: any) => d[0].label); + bandLabels.forEach((label: any) => { + expect(label.show).toBe(true); + expect(label.position).toBe('insideTopRight'); + }); + const rangeSeries = series.filter((x: any) => x.symbol === 'rect'); + rangeSeries.forEach((x: any) => { + expect(x.data[0].label).toBeUndefined(); + }); + + // labels hide with the toggle off or when the range has no label + const { echartOptions: unlabelled } = transformProps( + chartProps({ showLabels: false }), + ); + (unlabelled as any).series[0].markArea.data.forEach((d: any) => { + expect(d[0].label.show).toBe(false); + }); +}); + +test('reserves more grid space when the legend wraps onto extra rows', () => { + const wide = transformProps(chartProps({ showLegend: true })) + .echartOptions as any; + const narrow = transformProps( + new ChartProps({ + width: 220, + height: 200, + formData: { ...formData, showLegend: true }, + theme: supersetTheme, + queriesData: [{ data: [{ sum__num: 120 }] }], + hooks: {}, + }) as unknown as EchartsBulletChartProps, + ).echartOptions as any; + + expect(narrow.grid.top).toBeGreaterThan(wide.grid.top); + // without a legend the grid keeps its slim top margin + const noLegend = transformProps(chartProps()).echartOptions as any; + expect(noLegend.grid.top).toBeLessThan(wide.grid.top); +}); + test('sanitizes HTML in metric and marker labels before rendering tooltips', () => { const { echartOptions } = transformProps( chartProps({ From 1aeff038d7eb795ac63c2989ef632f249642fe46 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 22 Jul 2026 19:11:42 -0700 Subject: [PATCH 66/84] test(timeseries): cover the percent-change flag end-to-end from migrated params Adds transformProps-level coverage for rebase_percent_change: series rebased from the first point with a forced percent axis format, and the snake_case key the compare-chart migration stores read through real ChartProps camelization. Also guards the rebase against form data with no explicit x-axis by falling back to the temporal alias, matching extractSeries. Co-Authored-By: Claude Fable 5 --- .../src/Timeseries/transformProps.ts | 5 +- .../test/Timeseries/transformProps.test.ts | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 6de6eb95b2b6..3477f181d793 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -26,6 +26,7 @@ import { buildCustomFormatters, CategoricalColorNamespace, CurrencyFormatter, + DTTM_ALIAS, ensureIsArray, tooltipHtml, getCustomFormatter, @@ -307,7 +308,9 @@ export default function transformProps( (formData as { rebasePercentChange?: boolean }).rebasePercentChange, ); const rebasedData = rebasePercentChange - ? rebaseToPercentChange(forecastRebasedData, xAxisLabel) + ? // the same temporal-alias fallback extractSeries applies, so a chart + // with no explicit x-axis cannot have its x column rebased as data + rebaseToPercentChange(forecastRebasedData, xAxisLabel || DTTM_ALIAS) : forecastRebasedData; const isHorizontal = orientation === OrientationType.Horizontal; const { totalStackedValues, thresholdValues } = extractDataTotalValues( diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts index 9f51bcfcca82..98286cc1bed4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/transformProps.test.ts @@ -21,6 +21,7 @@ import { AnnotationStyle, AnnotationType, AxisType, + ChartProps, ComparisonType, DataRecord, EventAnnotationLayer, @@ -32,6 +33,7 @@ import { TimeGranularity, } from '@superset-ui/core'; import { GenericDataType } from '@apache-superset/core/common'; +import { supersetTheme } from '@apache-superset/core/theme'; import type { SeriesOption } from 'echarts'; import transformProps from '../../src/Timeseries/transformProps'; import { @@ -1978,3 +1980,66 @@ test('tooltip time grain wiring: chart-level time grain drives the tooltip when expect(result).toContain('2021'); expect(result).not.toContain('2021-01-07'); }); + +test('rebases each series to its percent change when the flag is enabled', () => { + const chartProps = createTestChartProps({ + formData: { + ...formData, + rebasePercentChange: true, + } as unknown as Partial, + }); + const transformed = transformProps(chartProps); + + // SF: 1 -> 3 rebases to 0 -> 2; NY: 2 -> 4 rebases to 0 -> 1 + expect(transformed.echartOptions).toEqual( + expect.objectContaining({ + series: expect.arrayContaining([ + expect.objectContaining({ + name: 'San Francisco', + data: [ + [BASE_TIMESTAMP, 0], + [BASE_TIMESTAMP + 300000000, 2], + ], + }), + expect.objectContaining({ + name: 'New York', + data: [ + [BASE_TIMESTAMP, 0], + [BASE_TIMESTAMP + 300000000, 1], + ], + }), + ]), + }), + ); + + // percent-change view forces a percent axis format + expect(getYAxisFormatter(transformed)(1, 0)).toContain('%'); +}); + +test('honors the snake_case flag the compare-chart migration stores in params', () => { + // MigrateCompareChart writes `rebase_percent_change` into slice params; + // ChartProps camelizes stored form data before transformProps reads it, so + // this exercises the full migrated-chart path rather than the camelized + // key the test helper injects directly. + const chartProps = new ChartProps({ + formData: { + datasource: '3__table', + viz_type: 'echarts_timeseries_line', + granularity_sqla: 'ds', + rebase_percent_change: true, + }, + width: 800, + height: 600, + queriesData, + theme: supersetTheme, + datasource: {}, + }) as unknown as EchartsTimeseriesChartProps; + const { echartOptions } = transformProps(chartProps); + + const { series } = echartOptions as unknown as { series: SeriesOption[] }; + const sanFrancisco = series.find(s => s.name === 'San Francisco'); + expect(sanFrancisco?.data).toEqual([ + [BASE_TIMESTAMP, 0], + [BASE_TIMESTAMP + 300000000, 2], + ]); +}); From 5bbe3ac750a1951abc0dc982879daf10f21c48a7 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 23 Jul 2026 22:36:32 -0700 Subject: [PATCH 67/84] fix(rose): use the theme text color for chart and legend text The nvd3-fork legend and axis labels render SVG text with the default black fill, which is unreadable against a dark theme background. Co-Authored-By: Claude Fable 5 --- superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx b/superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx index fde642a9b670..da0df5926571 100644 --- a/superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx +++ b/superset-frontend/plugins/plugin-chart-rose/src/ReactRose.tsx @@ -69,6 +69,7 @@ export default styled(Rose)` .superset-legacy-chart-rose text { font-size: ${theme.fontSizeSM}px; font-family: ${theme.fontFamily}; + fill: ${theme.colorText}; pointer-events: none; } From a4fc9ae4935724170110ea362a5b27690088ae2d Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 24 Jul 2026 02:57:52 -0700 Subject: [PATCH 68/84] chore(viz)!: delete the unregistered preset-chart-nvd3 package Nothing has registered this package since the Bullet and Time-series Period Pivot charts moved to ECharts; remove the workspace, its tsconfig reference, the custom-rules grandfather entry, and its lockfile entries (which also sweeps the nvd3-only fast-safe-stringify dependency). The nvd3-fork library itself remains a dependency of plugin-chart-rose, whose legend and tooltip still use it. Co-Authored-By: Claude Fable 5 --- superset-frontend/package-lock.json | 34 - superset-frontend/package.json | 1 - .../plugins/preset-chart-nvd3/CHANGELOG.md | 113 -- .../plugins/preset-chart-nvd3/README.md | 64 -- .../plugins/preset-chart-nvd3/package.json | 49 - .../src/Bullet/buildQuery.ts | 33 - .../src/Bullet/controlPanel.ts | 98 -- .../src/Bullet/images/example-dark.jpg | Bin 22213 -> 0 bytes .../src/Bullet/images/example.jpg | Bin 23045 -> 0 bytes .../src/Bullet/images/thumbnail-dark.png | Bin 3653 -> 0 bytes .../src/Bullet/images/thumbnail.png | Bin 978 -> 0 bytes .../src/Bullet/images/thumbnailLarge.png | Bin 2362 -> 0 bytes .../preset-chart-nvd3/src/Bullet/index.ts | 51 - .../src/Bullet/stories/NVD3Bullet.stories.tsx | 87 -- .../src/Bullet/stories/data.ts | 29 - .../preset-chart-nvd3/src/NVD3Controls.tsx | 549 --------- .../plugins/preset-chart-nvd3/src/NVD3Vis.ts | 1018 ----------------- .../preset-chart-nvd3/src/PropTypes.ts | 82 -- .../preset-chart-nvd3/src/ReactNVD3.tsx | 189 --- .../src/TimePivot/buildQuery.ts | 34 - .../src/TimePivot/controlPanel.ts | 134 --- .../src/TimePivot/images/example-dark.jpg | Bin 137758 -> 0 bytes .../src/TimePivot/images/example.jpg | Bin 131853 -> 0 bytes .../src/TimePivot/images/thumbnail-dark.png | Bin 25338 -> 0 bytes .../src/TimePivot/images/thumbnail.png | Bin 11158 -> 0 bytes .../src/TimePivot/images/thumbnailLarge.png | Bin 26764 -> 0 bytes .../preset-chart-nvd3/src/TimePivot/index.ts | 51 - .../src/TimePivot/transformData.ts | 179 --- .../src/TimePivot/transformProps.ts | 39 - .../plugins/preset-chart-nvd3/src/index.ts | 22 - .../plugins/preset-chart-nvd3/src/preset.ts | 33 - .../preset-chart-nvd3/src/transformProps.ts | 215 ---- .../plugins/preset-chart-nvd3/src/utils.ts | 354 ------ .../preset-chart-nvd3/src/utils/isTruthy.ts | 28 - .../preset-chart-nvd3/src/utils/tokenize.ts | 33 - .../src/vendor/superset/AnnotationTypes.ts | 86 -- .../preset-chart-nvd3/test/Bullet.test.ts | 98 -- .../preset-chart-nvd3/test/TimePivot.test.ts | 95 -- .../preset-chart-nvd3/test/tsconfig.json | 9 - .../preset-chart-nvd3/test/utils.test.ts | 358 ------ .../test/utils/isTruthy.test.ts | 57 - .../test/utils/tokenize.test.ts | 76 -- .../test/xAxisFormatter.test.ts | 30 - .../plugins/preset-chart-nvd3/tsconfig.json | 21 - .../preset-chart-nvd3/types/external.d.ts | 49 - .../scripts/check-custom-rules.js | 1 - superset-frontend/tsconfig.json | 1 - 47 files changed, 4400 deletions(-) delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/README.md delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/package.json delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example-dark.jpg delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example.jpg delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnailLarge.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example-dark.jpg delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example.jpg delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail-dark.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnailLarge.png delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/index.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/index.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/preset.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/utils.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/utils/isTruthy.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/utils/tokenize.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/test/xAxisFormatter.test.ts delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/tsconfig.json delete mode 100644 superset-frontend/plugins/preset-chart-nvd3/types/external.d.ts diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index a28dd17842e9..92c8b9919237 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -69,7 +69,6 @@ "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", "@superset-ui/preset-chart-deckgl": "file:./plugins/preset-chart-deckgl", - "@superset-ui/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", "@types/d3-selection": "^3.0.11", @@ -10842,10 +10841,6 @@ "resolved": "plugins/preset-chart-deckgl", "link": true }, - "node_modules/@superset-ui/preset-chart-nvd3": { - "resolved": "plugins/preset-chart-nvd3", - "link": true - }, "node_modules/@superset-ui/switchboard": { "resolved": "packages/superset-ui-switchboard", "link": true @@ -20804,12 +20799,6 @@ "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "license": "(MIT AND Zlib)" }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", @@ -44717,29 +44706,6 @@ "engines": { "node": ">=12" } - }, - "plugins/preset-chart-nvd3": { - "name": "@superset-ui/preset-chart-nvd3", - "version": "0.20.3", - "license": "Apache-2.0", - "dependencies": { - "d3": "^3.5.17", - "d3-tip": "^0.9.1", - "dompurify": "^3.4.11", - "fast-safe-stringify": "^2.1.1", - "lodash": "^4.18.1", - "lodash-es": "^4.18.1", - "nvd3-fork": "^2.0.5", - "prop-types": "^15.8.1", - "urijs": "^1.19.11" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "dayjs": "^1.11.21", - "react": "^18.3.0" - } } } } diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 0d6c4f4e7c6a..6cf4063f50d4 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -145,7 +145,6 @@ "@superset-ui/plugin-chart-partition": "file:./plugins/plugin-chart-partition", "@superset-ui/plugin-chart-rose": "file:./plugins/plugin-chart-rose", "@superset-ui/plugin-chart-world-map": "file:./plugins/plugin-chart-world-map", - "@superset-ui/preset-chart-nvd3": "file:./plugins/preset-chart-nvd3", "@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table", "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", diff --git a/superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md b/superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md deleted file mode 100644 index 16a3d4ced1c8..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/CHANGELOG.md +++ /dev/null @@ -1,113 +0,0 @@ - - -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -# [0.20.0](https://github.com/apache/superset/compare/v2021.41.0...v0.20.0) (2024-09-09) - -### Bug Fixes - -- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563)) -- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614)) -- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687)) -- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964)) -- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d)) -- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220)) -- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b)) -- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3)) -- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b)) -- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba)) -- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5)) -- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a)) -- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4)) -- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b)) - -### Features - -- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6)) -- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442)) -- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56)) -- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3)) -- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6)) -- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97)) -- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92)) -- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70)) -- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11)) -- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c)) -- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638)) -- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b)) -- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9)) -- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719)) -- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082)) - -# [0.19.0](https://github.com/apache/superset/compare/v2021.41.0...v0.19.0) (2024-09-07) - -### Bug Fixes - -- adding missing examples for bubble chart, bullet chart, calendar heatmap chart and country map chart in the gallery ([#22523](https://github.com/apache/superset/issues/22523)) ([839ec7c](https://github.com/apache/superset/commit/839ec7ceacc66c65928fd0ddead2b014db3d5563)) -- Adds the Deprecated label to Time-series Percent Change chart ([#30148](https://github.com/apache/superset/issues/30148)) ([5e42d7a](https://github.com/apache/superset/commit/5e42d7aed0d11c7aac91ab19088d2632e49da614)) -- **area chart legacy:** tool tip shows actual value rather than y axi… ([#23469](https://github.com/apache/superset/issues/23469)) ([db9ca20](https://github.com/apache/superset/commit/db9ca20737fecda8eee342b34d62d3b700ef3687)) -- **Dashboard:** Color inconsistency on refreshes and conflicts ([#27439](https://github.com/apache/superset/issues/27439)) ([313ee59](https://github.com/apache/superset/commit/313ee596f5435894f857d72be7269d5070c8c964)) -- **explore:** Fix chart standalone URL for report/thumbnail generation ([#20673](https://github.com/apache/superset/issues/20673)) ([84d4302](https://github.com/apache/superset/commit/84d4302628d18aa19c13cc5322e68abbc690ea4d)) -- **explore:** make SORT-Descending visible if Sort-by has value ([#17726](https://github.com/apache/superset/issues/17726)) ([d5768ab](https://github.com/apache/superset/commit/d5768ab649a70fd4f541ad4982498f622160b220)) -- **explore:** Prevent shared controls from checking feature flags outside React render ([#21315](https://github.com/apache/superset/issues/21315)) ([2285ebe](https://github.com/apache/superset/commit/2285ebe72ec4edded6d195052740b7f9f13d1f1b)) -- **legacy-chart:** corrupted raw chart data ([#24850](https://github.com/apache/superset/issues/24850)) ([1c5971d](https://github.com/apache/superset/commit/1c5971d3afb70a338444c41943ff90c3a9c03ec3)) -- Rename legacy line and area charts ([#28113](https://github.com/apache/superset/issues/28113)) ([b4c4ab7](https://github.com/apache/superset/commit/b4c4ab7790cbeb8d65ec7c1084482c21932e755b)) -- Reset sorting bar issue in Barchart ([#19371](https://github.com/apache/superset/issues/19371)) ([94e06c2](https://github.com/apache/superset/commit/94e06c2b6a1f782133bb9ef85a1d46ce7eacf9ba)) -- **storybook:** fix broken Storybook stories during development ([#29587](https://github.com/apache/superset/issues/29587)) ([462cda4](https://github.com/apache/superset/commit/462cda400baa00b3bcc4a7f8aded362ca55e18a5)) -- Tooltip of area chart shows undefined total ([#24916](https://github.com/apache/superset/issues/24916)) ([ec9e9a4](https://github.com/apache/superset/commit/ec9e9a46f2f092ce56d3ed5a8a9a3ea0214db88a)) -- warning of nth-child ([#23638](https://github.com/apache/superset/issues/23638)) ([16cc089](https://github.com/apache/superset/commit/16cc089b198dcdebc2422845aa08d18233c6b3a4)) -- Zero values on Dual Line axis bounds ([#23649](https://github.com/apache/superset/issues/23649)) ([d66e6e6](https://github.com/apache/superset/commit/d66e6e6d400db0fee35d73cd43e610cd1c491f4b)) - -### Features - -- Adds the ECharts Bubble chart ([#22107](https://github.com/apache/superset/issues/22107)) ([c81c60c](https://github.com/apache/superset/commit/c81c60c91fbcb09dd63c05f050e18ee09ceebfd6)) -- apply standardized form data to tier 2 charts ([#20530](https://github.com/apache/superset/issues/20530)) ([de524bc](https://github.com/apache/superset/commit/de524bc59f011fd361dcdb7d35c2cb51f7eba442)) -- **chart & legend:** make to enable show legend by default ([#19927](https://github.com/apache/superset/issues/19927)) ([7b3d0f0](https://github.com/apache/superset/commit/7b3d0f040b050905f7d0901d0227f1cd6b761b56)) -- **explore:** Apply denormalization to tier 2 charts form data ([#20524](https://github.com/apache/superset/issues/20524)) ([e12ee59](https://github.com/apache/superset/commit/e12ee59b13822241dca8d8015f1222c477edd4f3)) -- **explore:** Denormalize form data in echarts, world map and nvd3 bar and line charts ([#20313](https://github.com/apache/superset/issues/20313)) ([354a899](https://github.com/apache/superset/commit/354a89950c4d001da3e107f60788cea873bd6bf6)) -- **explore:** improve UI in the control panel ([#19748](https://github.com/apache/superset/issues/19748)) ([e3a54aa](https://github.com/apache/superset/commit/e3a54aa3c15bdd0c970aa73f898288a408205c97)) -- **explore:** standardized controls for time pivot chart ([#21321](https://github.com/apache/superset/issues/21321)) ([79525df](https://github.com/apache/superset/commit/79525dfaf29b810af668e3b6c5a56cd866370d92)) -- **formatters:** Add custom d3-time-format locale ([#24263](https://github.com/apache/superset/issues/24263)) ([024cfd8](https://github.com/apache/superset/commit/024cfd86e408ec5f7ddf49a9e90908e2fb2e6b70)) -- improve color consistency (save all labels) ([#19038](https://github.com/apache/superset/issues/19038)) ([dc57508](https://github.com/apache/superset/commit/dc575080d7e43d40b1734bb8f44fdc291cb95b11)) -- **legacy-preset-chart-nvd3:** add richtooltip in nvd3 bar chart ([#17615](https://github.com/apache/superset/issues/17615)) ([72f3215](https://github.com/apache/superset/commit/72f3215ffc74ead33dba57196aeaf4e1db63fd6c)) -- Migrates Dual Line Chart to Mixed Chart ([#23910](https://github.com/apache/superset/issues/23910)) ([f5148ef](https://github.com/apache/superset/commit/f5148ef728ce649697c10fb7aa65982d7dd05638)) -- Removes the Multiple Line Charts ([#23933](https://github.com/apache/superset/issues/23933)) ([6ce8592](https://github.com/apache/superset/commit/6ce85921fc103ba0e93b437d473003e6f1b4a42b)) -- update time comparison choices (again) ([#17968](https://github.com/apache/superset/issues/17968)) ([05d9cde](https://github.com/apache/superset/commit/05d9cde203b99f8c63106446f0be58668cc9f0c9)) -- update time comparison choices (again) ([#22458](https://github.com/apache/superset/issues/22458)) ([9e81c3a](https://github.com/apache/superset/commit/9e81c3a1192a18226d505178d16e1e395917a719)) -- **viz picker:** Remove some tags, refactor Recommended section ([#27708](https://github.com/apache/superset/issues/27708)) ([c314999](https://github.com/apache/superset/commit/c3149994ac0d4392e0462421b62cd0c034142082)) - -# [0.18.0](https://github.com/apache-superset/superset-ui/compare/v0.17.87...v0.18.0) (2021-08-30) - -**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3 - -## [0.17.63](https://github.com/apache-superset/superset-ui/compare/v0.17.62...v0.17.63) (2021-07-02) - -**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3 - -## [0.17.62](https://github.com/apache-superset/superset-ui/compare/v0.17.61...v0.17.62) (2021-07-02) - -**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3 - -## [0.17.61](https://github.com/apache-superset/superset-ui/compare/v0.17.60...v0.17.61) (2021-07-02) - -**Note:** Version bump only for package @superset-ui/legacy-preset-chart-nvd3 diff --git a/superset-frontend/plugins/preset-chart-nvd3/README.md b/superset-frontend/plugins/preset-chart-nvd3/README.md deleted file mode 100644 index f09ba2661224..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/README.md +++ /dev/null @@ -1,64 +0,0 @@ - - -## @superset-ui/preset-chart-nvd3 - -[![Version](https://img.shields.io/npm/v/@superset-ui/preset-chart-nvd3.svg?style=flat)](https://www.npmjs.com/package/@superset-ui/preset-chart-nvd3) -[![Libraries.io](https://img.shields.io/librariesio/release/npm/%40superset-ui%2Fpreset-chart-nvd3?style=flat)](https://libraries.io/npm/@superset-ui%2Fpreset-chart-nvd3) - -This plugin provides Big Number for Superset. - -### Usage - -Import the preset and register. This will register all the chart plugins under nvd3. - -```js -import { NVD3ChartPreset } from '@superset-ui/preset-chart-nvd3'; - -new NVD3ChartPreset().register(); -``` - -or register charts one by one. Configure `key`, which can be any `string`, and register the plugin. -This `key` will be used to lookup this chart throughout the app. - -```js -import { - AreaChartPlugin, - LineChartPlugin, -} from '@superset-ui/preset-chart-nvd3'; - -new AreaChartPlugin().configure({ key: 'area' }).register(); -new LineChartPlugin().configure({ key: 'line' }).register(); -``` - -Then use it via `SuperChart`. See -[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-nvd3) -for more details. - -```js - -``` diff --git a/superset-frontend/plugins/preset-chart-nvd3/package.json b/superset-frontend/plugins/preset-chart-nvd3/package.json deleted file mode 100644 index 2c39a92f9b79..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@superset-ui/preset-chart-nvd3", - "version": "0.20.3", - "description": "Superset Legacy Chart - NVD3", - "sideEffects": [ - "*.css" - ], - "main": "lib/index.js", - "module": "esm/index.js", - "files": [ - "esm", - "lib" - ], - "repository": { - "type": "git", - "url": "https://github.com/apache/superset.git", - "directory": "superset-frontend/plugins/preset-chart-nvd3" - }, - "keywords": [ - "superset" - ], - "author": "Superset", - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/apache/superset/issues" - }, - "homepage": "https://github.com/apache/superset/tree/master/superset-frontend/plugins/preset-chart-nvd3#readme", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "d3": "^3.5.17", - "d3-tip": "^0.9.1", - "fast-safe-stringify": "^2.1.1", - "lodash": "^4.18.1", - "nvd3-fork": "^2.0.5", - "dompurify": "^3.4.12", - "prop-types": "^15.8.1", - "urijs": "^1.19.11", - "lodash-es": "^4.18.1" - }, - "peerDependencies": { - "@apache-superset/core": "*", - "@superset-ui/chart-controls": "*", - "@superset-ui/core": "*", - "dayjs": "^1.11.21", - "react": "^18.3.0" - } -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts deleted file mode 100644 index 2db2b125834f..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/buildQuery.ts +++ /dev/null @@ -1,33 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -/** - * Mirrors the legacy BulletViz.query_obj: a single ungrouped metric. - */ -export default function buildQuery(formData: QueryFormData) { - const { metric } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - columns: [], - metrics: metric ? [metric] : [], - }, - ]); -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts deleted file mode 100644 index 0ef99b890aa8..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/controlPanel.ts +++ /dev/null @@ -1,98 +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 { t } from '@apache-superset/core/translation'; -import { ControlPanelConfig } from '@superset-ui/chart-controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - { - label: t('Query'), - expanded: true, - controlSetRows: [['metric'], ['adhoc_filters']], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - { - name: 'ranges', - config: { - type: 'TextControl', - label: t('Ranges'), - default: '', - description: t('Ranges to highlight with shading'), - }, - }, - { - name: 'range_labels', - config: { - type: 'TextControl', - label: t('Range labels'), - default: '', - description: t('Labels for the ranges'), - }, - }, - ], - [ - { - name: 'markers', - config: { - type: 'TextControl', - label: t('Markers'), - default: '', - description: t('List of values to mark with triangles'), - }, - }, - { - name: 'marker_labels', - config: { - type: 'TextControl', - label: t('Marker labels'), - default: '', - description: t('Labels for the markers'), - }, - }, - ], - [ - { - name: 'marker_lines', - config: { - type: 'TextControl', - label: t('Marker lines'), - default: '', - description: t('List of values to mark with lines'), - }, - }, - { - name: 'marker_line_labels', - config: { - type: 'TextControl', - label: t('Marker line labels'), - default: '', - description: t('Labels for the marker lines'), - }, - }, - ], - ], - }, - ], -}; - -export default config; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example-dark.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/example-dark.jpg deleted file mode 100644 index 3b6cf2a7626a8c18d078e58c148efa7e9f5be664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22213 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 gZr$u+U>5_s7}&+YE(Ufnu#16R4E%4x0Aut&0A$ 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 PIIw~LM{R(MHt|0I{|a^o diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail-dark.png deleted file mode 100644 index d67e71c281d556ff553d061f6f1ba95ba8f01b26..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3653 zcmeHKX;4#H8hz-DrV#}P22f;hK!buTp&_UUsNg~a5lqvF0g<4hgct-uNJ9wP>L4KL zQGv#g#H9g|C1MC`AW*U+L?IFrmLRJj0SO6G)9B1xYL$Cn${ z&ggXY*)R{&Z7kN~3j9RO?wUupmV0f4>W2t@kNKYix%1Tf~$hwKGMpN#%t`3FqD zH23QweMVXzAF`qr^6S|6c-zk${?gpP8}Fy8_Kz@qe*OO*2?CgWmTW!-@SAk-p2L3u z?{6&V2RDSjsOX0@LT|RX!l`x;Xmwp(ofIz|m*wWwpo`B!zVHC;)fj%R!rxMNhyPhE zk5CsEm#bcP&Va=r`=^&xyy6(D`;hnqV}G_0cxy;PH>fzLEVV> z;=8vnKj*{RUi0t(<;{yAnjt5gZFb0FGVWY5x>z4$k4vuUr>}`$A}>Un_KYp818ViA zG)Wn%CFOlbYsv@*+@crfMILWyx)(cJ`e3`v)wMp=&?;q73ND8vSSe9Z@zR{n5>6&E zDbf8S4|^N0AOhE#QJOICrDlzY2cMwq%5~$m8^?!g=m}nmHi#F_gz!BUZBq>$(xa-J z7nC_WBS*vPg{q$xKI z6t~KENW=C*!3PxA-Ac*9!Kf$#yXnSg&T!K+Fv}f&)~3eB>3tFIi?ZtcjIA`{gwTW5 zrPCK6DK0O9f77p!NTbauA)Kb~TTb1>`?>I~a8?XNMU615lta&U=)d&_wTsGs;}8eW z6A&h{a=Zmn6x9e}Lv3YykgD_IF3czh%tQ~UwzKlpcP5q?4$JRRco5Ox%{0fZyI?#q zF;PTxzro9iut2uw$O!AMGEdu0effLfe*aFwB+7u#YV0CpDlsHLCE%s0n%d*laOrZ0cbl?i~EZF|aPTe)JcY zolr$ilBQWhmJ%E?(*1_;>!Oj4F?y2kJlApXaiaq@&dJDu5o=~_+#iSARtXPF0-Y8) z`=C5El>lc-gh+b3Y2@*Hc@9 z=y|tq8QTGUWOi;RC{6IggsF0;(E1(szd^AyPqmH9q-g0cv(H1-*74UKs0Op_`3kIZRj z|96iyw&8-Gwj%Yo=UvkieX3|;Kvk9KroZo1-e@0p&w6fb z5RSN%cLrHW=gNkI8IImR41|Gt15Zi|esFxgL14yI(|%e+tO&lWXzv8_Dp3ygPpR;jEQ;k2JS)=(Q#Ub;>26p}%2r9Q&Oe9i8?> zzvZyAUWb|C@&oBTTJuo_NjanG-d31BtUP~#CW4+4!XQG|8Sd6$ouN}nA@hVA9cP{= zrIr(CmKT~j#HO_N65*_|@gPQ}S*atS_IA#_T<*hrXV7413-VHv0J?{Yq0Ou552bNr zP@V1K1Y@vUkz8D|^z{=#$yN9lnPqtI(9Ms~NLxaQ;++`G)~CpJXhs`tUedsC@)9Wr z22P%038hBo+=^?`a0h)VczNAma@h)Ici30F;iHAqZ%I~IX9mxd;hFs^BP}$*99MM{ zls>0FrIh=koTmdeIG=W~!R-b9i|%{>@%t0lv;I%eC;mPDCkQIKpjw%+l}d*27IuM- O0o)ECT#NRfyz~?9bbN3C diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail.png b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/images/thumbnail.png deleted file mode 100644 index 38e9a733cf3a8fd34f1cf48c5727cb5883b66585..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 978 zcmeAS@N?(olHy`uVBq!ia0y~yU;;9k7+9Er6kFKIlR!#0z$e7@|Ns9_pFTZ(>eSUM zR~$Nae){yOb>%_ZwjEO@P3c*GeCz7P>-U{1%*%Ut>A;0YZ~wFUzhPitPVjVb45_&F z_WHr>*{L$@ANC#M4&pJH=CC5}m7c9s+ooo9$v3ycHKydSOTIW*pAa71u}4jrW1h{M zO^PYLnzIeU-13<@b3ZStm{p>yO z&+q`5 zPyhP1lWEoO-@AbVm#u?~{`S1PV;6Yf(C69b&u?yc{ePph{QuAcH9KRh{~2!heeKrA z`^)QN4=}}9GxurVxVbWRnfHTZC%qT^34DLwj@52P0ptGv@9(j&=QBoK?(sbTZzmr2KXmttbcbBK7>2FES~KP!k~G+Q{9z%FT+!yKQ%`-UG>=}H^8bgy z9N%As5_;P&H7D)d_d#RL<+aV<5AXVy@;Eg@UZ?DEk>cxX+uL4?yPDmd)p_&DZ>8v} zkQDKGIeWN%e-nQ^qvY|^^NBu}{;Kpz^7JRaie}&XPK7(-ZXDm~n@hv2r)|8R)l&Mr z{h9f~>0$e1Pg&U9dS@fE@6O8OIh(>$yzNDARxLiUFT?EfhWCHdWG3g-Tzk!Te9y+c zwe~%dG7-i-mCKEbI3@?g>Mr|wd*kh@)q+TXQKY$S3aH zBdf@BUALD#{qg_lPA@C*YmsM`J$*XE_f+H=wW&(c>rx8uKfb+Sr-bihiP)LerDyJ( iP$WCW0yP>evuE7ALG;=7T?~F8b39%BT-G@yGywoSB~8Do#aMA((hc(fJmf>F4gh{_J#S-t(OEKF@jHGw+;} z8y<$YK-r-H02U#^2Mz;ZVpy60GlT(xJLSy=Se+akMFC*VI%B{9yUf;*bPo@W41^$P zr_(XX9F*g3k7Muxa@)|TwGxTs_3PI%nM~Vu5?XrEnYd%=RaJPix^QUG9$(Y(j648< zSwqMH|H$-V_0R*0D{kh2{#!2<2@@b?3dn_<*Vo^wgJGBNx*E!S8UOzd;D*QJD^lV- zHOGL-c(yhMnBaixzfC3pwiX;RvKow-$T|Wf6TXQqjzX}tPgh_I{|d-Z;wR|JZZE$& zVmW~0^1zH7ExvZ%cc;Pu7-)DQLA)2*FjWSywg7&&g_8C-ajU;A4mtG58au7WPVRPh zX0{xWGL#rjYajnZAiXrHG~TYT8aRUjPtb(zs6(a)Au0`9lz5OJeoXEMB7p^I&D6D3 zI9NDXPh&JfiUC+LdVGfk4Q|v~oq{?z2xp8q!VVyfnk^(C;vmxPyK6lF%Baay%!9El zL|FNNK`uh6?})Rz91y;>Mn8ZYZ*m6xFQ66WM-tAE5kscNV-vv0Z(i|07>!enx1zy~ z91r>F;e3Qjx<3f}>0ycL-PcB)d|vuSPuoalNZxVkMQwiEMoF1|NbizBiOH`|{x(Oo z&`NxnX%*Cx$19yr@^H|_NsFyLX?&lxy?#?TkJ0Ny%Zk#ISaD+o-E;HBQaUR>9fJ?F zIWyEZl74kT7$eAfVOgpuB97_fEvuvG_mp$Z^G8eQjR|}&1)sE>|Gt^ ztn{n#yg#TyOz{%>ap^9GG*r3Xd-BX>n=xjJQ?8CGhxR|=1{SI0RUx`tSu*~7h8F+y z-cqKlf)3q2nwL0580?m+DZfjLuT&GWT8YE$rhTDVlY-gQnxC1=5lzuO-R-GZ2BH1o zz%wSVOr;+SzQFQe?@n^KS$|jE5X%ejnVil%;r*+wLAt4jtIpj+o_VWnd-Ljc<=pdS zG@hdQKFyCKRS!-(v*WWAs^0!kzvXwVc2Io&PUO_#l*{~D>UY~*Twjs+jPjU=@Nf;f z&5L~;3=hR#CBe*e4Ix|p;-YE33cKE%OcsW|M{7r2ywBH)qgb|^g5!(DQZKA3JPDBt z1RM0_>Kx+nE<$x3X3}xUegYYIpE$ervb} zA0fC-QDMYiW5iyfnGb(Y3ueXg=ffQwm(b2;EzEq@CV71?Ke}mg<{}^nnK1*Ia%FO! zZnQdNB@#OaZ)#yn-fX6lj2~SsYG>9DzVTugXkpu=GiPrMjOum;4m1XwzVr`)CYf*m diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts deleted file mode 100644 index 7088749c8991..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/index.ts +++ /dev/null @@ -1,51 +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 { t } from '@apache-superset/core/translation'; -import { ChartMetadata, ChartPlugin } from '@superset-ui/core'; -import transformProps from '../transformProps'; -import example from './images/example.jpg'; -import exampleDark from './images/example-dark.jpg'; -import thumbnail from './images/thumbnail.png'; -import thumbnailDark from './images/thumbnail-dark.png'; -import controlPanel from './controlPanel'; - -const metadata = new ChartMetadata({ - category: t('KPI'), - credits: ['http://nvd3.org'], - description: t( - 'Showcases the progress of a single metric against a given target. The higher the fill, the closer the metric is to the target.', - ), - exampleGallery: [{ url: example, urlDark: exampleDark }], - name: t('Bullet Chart'), - tags: [t('Business'), t('Report'), t('nvd3')], - thumbnail, - thumbnailDark, -}); - -export default class BulletChartPlugin extends ChartPlugin { - constructor() { - super({ - loadChart: () => import('../ReactNVD3'), - loadBuildQuery: () => import('./buildQuery'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx deleted file mode 100644 index acf724a9e18e..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/NVD3Bullet.stories.tsx +++ /dev/null @@ -1,87 +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 { BulletChartPlugin } from '@superset-ui/preset-chart-nvd3'; -import { dummyDatasource, withResizableChartDemo } from '@storybook-shared'; -import data from './data'; - -new BulletChartPlugin().configure({ key: VizType.Bullet }).register(); - -export default { - title: 'Chart Plugins/preset-chart-nvd3/Bullet', - decorators: [withResizableChartDemo], - args: { - ranges: '0, 50, 75, 100', - rangeLabels: 'Low, Medium, High', - markers: '65', - markerLabels: 'Target', - }, - argTypes: { - ranges: { - control: 'text', - description: 'Comma-separated range values', - }, - rangeLabels: { - control: 'text', - description: 'Comma-separated range labels', - }, - markers: { - control: 'text', - description: 'Comma-separated marker values', - }, - markerLabels: { - control: 'text', - description: 'Comma-separated marker labels', - }, - }, -}; - -export const Basic = ({ - ranges, - rangeLabels, - markers, - markerLabels, - width, - height, -}: { - ranges: string; - rangeLabels: string; - markers: string; - markerLabels: string; - width: number; - height: number; -}) => ( - -); diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts b/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts deleted file mode 100644 index 1c4cf5f6ea33..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/Bullet/stories/data.ts +++ /dev/null @@ -1,29 +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 */ -export default { - measures: [75691], - ranges: [0, 83260.1], - rangeLabels: null, - markers: null, - markerLabels: null, - markerLines: null, - markerLineLabels: null, -}; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx deleted file mode 100644 index 9c45ba8d847e..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Controls.tsx +++ /dev/null @@ -1,549 +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 react/jsx-key */ - -import { t } from '@apache-superset/core/translation'; -import { - ControlPanelSectionConfig, - ControlSubSectionHeader, - CustomControlItem, - D3_TIME_FORMAT_OPTIONS, - D3_FORMAT_DOCS, - D3_FORMAT_OPTIONS, - DEFAULT_TIME_FORMAT, -} from '@superset-ui/chart-controls'; - -/* - Plugins in question: - - AreaChartPlugin, - BarChartPlugin, - BubbleChartPlugin, - BulletChartPlugin, - CompareChartPlugin, - DistBarChartPlugin, - DualLineChartPlugin, - LineChartPlugin, - LineMultiChartPlugin, - PieChartPlugin, - TimePivotChartPlugin, -*/ - -export const yAxis2Format: CustomControlItem = { - name: 'y_axis_2_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Right Axis Format'), - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - description: D3_FORMAT_DOCS, - }, -}; - -export const showMarkers: CustomControlItem = { - name: 'show_markers', - config: { - type: 'CheckboxControl', - label: t('Show Markers'), - renderTrigger: true, - default: false, - description: t('Show data points as circle markers on the lines'), - }, -}; - -export const leftMargin: CustomControlItem = { - name: 'left_margin', - config: { - type: 'SelectControl', - freeForm: true, - clearable: false, - label: t('Left Margin'), - choices: [ - ['auto', t('auto')], - [50, '50'], - [75, '75'], - [100, '100'], - [125, '125'], - [150, '150'], - [200, '200'], - ], - default: 'auto', - renderTrigger: true, - description: t( - 'Left margin, in pixels, allowing for more room for axis labels', - ), - }, -}; - -export const yAxisShowMinmax: CustomControlItem = { - name: 'y_axis_showminmax', - config: { - type: 'CheckboxControl', - label: t('Y bounds'), - renderTrigger: true, - default: false, - description: t('Whether to display the min and max values of the Y-axis'), - }, -}; - -export const yAxis2ShowMinmax: CustomControlItem = { - name: 'y_axis_2_showminmax', - config: { - type: 'CheckboxControl', - label: t('Y 2 bounds'), - renderTrigger: true, - default: false, - description: t('Whether to display the min and max values of the Y-axis'), - }, -}; - -export const lineInterpolation: CustomControlItem = { - name: 'line_interpolation', - config: { - type: 'SelectControl', - label: t('Line Style'), - renderTrigger: true, - choices: [ - ['linear', t('linear')], - ['basis', t('basis')], - ['cardinal', t('cardinal')], - ['monotone', t('monotone')], - ['step-before', t('step-before')], - ['step-after', t('step-after')], - ], - default: 'linear', - description: t('Line interpolation as defined by d3.js'), - }, -}; - -export const showBrush: CustomControlItem = { - name: 'show_brush', - config: { - type: 'SelectControl', - label: t('Show Range Filter'), - renderTrigger: true, - clearable: false, - default: 'auto', - choices: [ - ['yes', t('Yes')], - ['no', t('No')], - ['auto', t('Auto')], - ], - description: t('Whether to display the time range interactive selector'), - }, -}; - -export const showLegend: CustomControlItem = { - name: 'show_legend', - config: { - type: 'CheckboxControl', - label: t('Legend'), - renderTrigger: true, - default: true, - description: t('Whether to display the legend (toggles)'), - }, -}; - -export const showControls: CustomControlItem = { - name: 'show_controls', - config: { - type: 'CheckboxControl', - label: t('Extra Controls'), - renderTrigger: true, - default: false, - description: t( - 'Whether to show extra controls or not. Extra controls ' + - 'include things like making multiBar charts stacked ' + - 'or side by side.', - ), - }, -}; - -export const xAxisLabel: CustomControlItem = { - name: 'x_axis_label', - config: { - type: 'TextControl', - label: t('X Axis Label'), - renderTrigger: true, - default: '', - }, -}; - -export const bottomMargin: CustomControlItem = { - name: 'bottom_margin', - config: { - type: 'SelectControl', - clearable: false, - freeForm: true, - label: t('Bottom Margin'), - choices: [ - ['auto', t('auto')], - [50, '50'], - [75, '75'], - [100, '100'], - [125, '125'], - [150, '150'], - [200, '200'], - ], - default: 'auto', - renderTrigger: true, - description: t( - 'Bottom margin, in pixels, allowing for more room for axis labels', - ), - }, -}; - -export const xTicksLayout: CustomControlItem = { - name: 'x_ticks_layout', - config: { - type: 'SelectControl', - label: t('X Tick Layout'), - choices: [ - ['auto', t('auto')], - ['flat', t('flat')], - ['45°', '45°'], - ['staggered', t('staggered')], - ], - default: 'auto', - clearable: false, - renderTrigger: true, - description: t('The way the ticks are laid out on the X-axis'), - }, -}; - -export const xAxisFormat: CustomControlItem = { - name: 'x_axis_format', - config: { - type: 'SelectControl', - freeForm: true, - label: t('X Axis Format'), - renderTrigger: true, - choices: D3_TIME_FORMAT_OPTIONS, - default: DEFAULT_TIME_FORMAT, - description: D3_FORMAT_DOCS, - }, -}; - -export const yLogScale: CustomControlItem = { - name: 'y_log_scale', - config: { - type: 'CheckboxControl', - label: t('Y Log Scale'), - default: false, - renderTrigger: true, - description: t('Use a log scale for the Y-axis'), - }, -}; - -export const yAxisBounds: CustomControlItem = { - name: 'y_axis_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis Bounds'), - renderTrigger: true, - default: [null, null], - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - }, -}; - -export const yAxis2Bounds: CustomControlItem = { - name: 'y_axis_2_bounds', - config: { - type: 'BoundsControl', - label: t('Y Axis 2 Bounds'), - renderTrigger: true, - default: [null, null], - description: t( - 'Bounds for the Y-axis. When left empty, the bounds are ' + - 'dynamically defined based on the min/max of the data. Note that ' + - "this feature will only expand the axis range. It won't " + - "narrow the data's extent.", - ), - }, -}; - -export const xAxisShowMinmax: CustomControlItem = { - name: 'x_axis_showminmax', - config: { - type: 'CheckboxControl', - label: t('X bounds'), - renderTrigger: true, - default: false, - description: t('Whether to display the min and max values of the X-axis'), - }, -}; - -export const richTooltip: CustomControlItem = { - name: 'rich_tooltip', - config: { - type: 'CheckboxControl', - label: t('Rich Tooltip'), - renderTrigger: true, - default: true, - description: t( - 'The rich tooltip shows a list of all series for that point in time', - ), - }, -}; - -export const showBarValue: CustomControlItem = { - name: 'show_bar_value', - config: { - type: 'CheckboxControl', - label: t('Bar Values'), - default: false, - renderTrigger: true, - description: t('Show the value on top of the bar'), - }, -}; - -export const barStacked: CustomControlItem = { - name: 'bar_stacked', - config: { - type: 'CheckboxControl', - label: t('Stacked Bars'), - renderTrigger: true, - default: false, - description: null, - }, -}; - -export const reduceXTicks: CustomControlItem = { - name: 'reduce_x_ticks', - config: { - type: 'CheckboxControl', - label: t('Reduce X ticks'), - renderTrigger: true, - default: false, - description: t( - 'Reduces the number of X-axis ticks to be rendered. ' + - 'If true, the x-axis will not overflow and labels may be ' + - 'missing. If false, a minimum width will be applied ' + - 'to columns and the width may overflow into an ' + - 'horizontal scroll.', - ), - }, -}; - -export const yAxisLabel: CustomControlItem = { - name: 'y_axis_label', - config: { - type: 'TextControl', - label: t('Y Axis Label'), - renderTrigger: true, - default: '', - }, -}; - -export const timeSeriesSection: ControlPanelSectionConfig[] = [ - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metrics'], - ['adhoc_filters'], - ['groupby'], - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - [ - { - name: 'contribution', - config: { - type: 'CheckboxControl', - label: t('Contribution'), - default: false, - description: t('Compute the contribution to the total'), - }, - }, - ], - ['row_limit', null], - ], - }, - { - label: t('Advanced Analytics'), - tabOverride: 'data', - description: t( - 'This section contains options ' + - 'that allow for advanced analytical post processing ' + - 'of query results', - ), - controlSetRows: [ - [ - - {t('Rolling Window')} - , - ], - [ - { - name: 'rolling_type', - config: { - type: 'SelectControl', - label: t('Rolling Function'), - default: 'None', - choices: [ - ['None', t('None')], - ['mean', t('mean')], - ['sum', t('sum')], - ['std', t('std')], - ['cumsum', t('cumsum')], - ], - description: t( - 'Defines a rolling window function to apply, works along ' + - 'with the [Periods] text box', - ), - }, - }, - ], - [ - { - name: 'rolling_periods', - config: { - type: 'TextControl', - label: t('Periods'), - isInt: true, - description: t( - 'Defines the size of the rolling window function, ' + - 'relative to the time granularity selected', - ), - }, - }, - ], - [ - { - name: 'min_periods', - config: { - type: 'TextControl', - label: t('Min Periods'), - isInt: true, - description: t( - 'The minimum number of rolling periods required to show ' + - 'a value. For instance if you do a cumulative sum on 7 days ' + - 'you may want your "Min Period" to be 7, so that all data points ' + - 'shown are the total of 7 periods. This will hide the "ramp up" ' + - 'taking place over the first 7 periods', - ), - }, - }, - ], - [ - - {t('Time Comparison')} - , - ], - [ - { - name: 'time_compare', - config: { - type: 'SelectControl', - multi: true, - freeForm: true, - label: t('Time Shift'), - choices: [ - ['1 day', t('1 day')], - ['1 week', t('1 week')], - ['28 days', t('28 days')], - ['30 days', t('30 days')], - ['52 weeks', t('52 weeks')], - ['1 year', t('1 year')], - ['104 weeks', t('104 weeks')], - ['2 years', t('2 years')], - ['156 weeks', t('156 weeks')], - ['3 years', t('3 years')], - ], - description: t( - 'Overlay one or more timeseries from a ' + - 'relative time period. Expects relative time deltas ' + - 'in natural language (example: 24 hours, 7 days, ' + - '52 weeks, 365 days). Free text is supported.', - ), - }, - }, - ], - [ - { - name: 'comparison_type', - config: { - type: 'SelectControl', - label: t('Calculation type'), - default: 'values', - choices: [ - ['values', t('Actual Values')], - ['absolute', t('Difference')], - ['percentage', t('Percentage change')], - ['ratio', t('Ratio')], - ], - description: t( - 'How to display time shifts: as individual lines; as the ' + - 'difference between the main time series and each time shift; ' + - 'as the percentage change; or as the ratio between series and time shifts.', - ), - }, - }, - ], - [{t('Resample')}], - [ - { - name: 'resample_rule', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Rule'), - default: null, - choices: [ - ['1T', t('1T')], - ['1H', t('1H')], - ['1D', t('1D')], - ['7D', t('7D')], - ['1M', t('1M')], - ['1AS', t('1AS')], - ], - description: t('Pandas resample rule'), - }, - }, - ], - [ - { - name: 'resample_method', - config: { - type: 'SelectControl', - freeForm: true, - label: t('Method'), - default: null, - choices: [ - ['asfreq', t('asfreq')], - ['bfill', t('bfill')], - ['ffill', t('ffill')], - ['median', t('median')], - ['mean', t('mean')], - ['sum', t('sum')], - ], - description: t('Pandas resample method'), - }, - }, - ], - ], - }, -]; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts b/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts deleted file mode 100644 index ca750f1c96d4..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/NVD3Vis.ts +++ /dev/null @@ -1,1018 +0,0 @@ -// @ts-nocheck -- legacy file heavily dependent on untyped d3 v3 and nvd3 APIs -/* eslint-disable react/sort-prop-types */ -/** - * 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 { kebabCase, throttle } from 'lodash-es'; -import d3 from 'd3'; -import utc from 'dayjs/plugin/utc'; -import nv from 'nvd3-fork'; -import PropTypes from 'prop-types'; -import { - CategoricalColorNamespace, - evalExpression, - getNumberFormatter, - getTimeFormatter, - isDefined, - NumberFormats, - SMART_DATE_VERBOSE_ID, - VizType, -} from '@superset-ui/core'; -import { t } from '@apache-superset/core/translation'; -import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates'; - -import 'nvd3-fork/build/nv.d3.css'; - -/* eslint-disable-next-line */ -import ANNOTATION_TYPES, { - applyNativeColumns, -} from './vendor/superset/AnnotationTypes'; -import isTruthy from './utils/isTruthy'; -import { - cleanColorInput, - computeYDomain, - drawBarValues, - generateTimePivotTooltip, - generateTooltipClassName, - getMaxLabelSize, - getTimeOrNumberFormatter, - hideTooltips, - tipFactory, - removeTooltip, - setAxisShowMaxMin, - stringifyTimeRange, - wrapTooltip, -} from './utils'; -import { - annotationLayerType, - boxPlotValueType, - bulletDataType, - categoryAndValueXYType, - rgbObjectType, - numericXYType, - numberOrAutoType, - stringOrObjectWithLabelType, -} from './PropTypes'; - -const NO_DATA_RENDER_DATA = [ - { text: 'No data', dy: '-.75em', class: 'header' }, - { - text: 'Adjust filters or check the Datasource.', - dy: '.75em', - class: 'body', - }, -]; - -dayjs.extend(utc); - -const smartDateVerboseFormatter = getTimeFormatter(SMART_DATE_VERBOSE_ID); - -// Override the noData render function to make a prettier UX -// Code adapted from https://github.com/novus/nvd3/blob/master/src/utils.js#L653 -nv.utils.noData = function noData(chart, container) { - const opt = chart.options(); - const margin = opt.margin(); - const height = nv.utils.availableHeight(null, container, margin); - const width = nv.utils.availableWidth(null, container, margin); - const x = margin.left + width / 2; - const y = margin.top + height / 2; - - // Remove any previously created chart components - container.selectAll('g').remove(); - - const noDataText = container - .selectAll('.nv-noData') - .data(NO_DATA_RENDER_DATA); - - noDataText - .enter() - .append('text') - .attr('class', d => `nvd3 nv-noData ${d.class}`) - .attr('dy', d => d.dy) - .style('text-anchor', 'middle'); - - noDataText - .attr('x', x) - .attr('y', y) - .text(d => d.text); -}; - -const { getColor, getScale } = CategoricalColorNamespace; - -// Limit on how large axes margins can grow as the chart window is resized -const MAX_MARGIN_PAD = 30; -const MIN_HEIGHT_FOR_BRUSH = 480; -const MAX_NO_CHARACTERS_IN_LABEL = 40; - -const BREAKPOINTS = { - small: 340, -}; - -const TIMESERIES_VIZ_TYPES = [VizType.TimePivot]; - -const CHART_ID_PREFIX = 'chart-id-'; - -const propTypes = { - data: PropTypes.oneOfType([ - PropTypes.arrayOf( - PropTypes.oneOfType([ - // pie - categoryAndValueXYType, - // dist-bar - PropTypes.shape({ - key: PropTypes.string, - values: PropTypes.arrayOf(categoryAndValueXYType), - }), - // area, line, compare, bar - PropTypes.shape({ - key: PropTypes.arrayOf(PropTypes.string), - values: PropTypes.arrayOf(numericXYType), - }), - // dual-line - PropTypes.shape({ - classed: PropTypes.string, - key: PropTypes.string, - type: PropTypes.string, - values: PropTypes.arrayOf(numericXYType), - yAxis: PropTypes.number, - }), - // box-plot - PropTypes.shape({ - label: PropTypes.string, - values: PropTypes.arrayOf(boxPlotValueType), - }), - // bubble - PropTypes.shape({ - key: PropTypes.string, - values: PropTypes.arrayOf(PropTypes.object), - }), - ]), - ), - bulletDataType, - ]), - width: PropTypes.number, - height: PropTypes.number, - annotationData: PropTypes.object, - annotationLayers: PropTypes.arrayOf(annotationLayerType), - bottomMargin: numberOrAutoType, - colorScheme: PropTypes.string, - comparisonType: PropTypes.string, - contribution: PropTypes.bool, - leftMargin: numberOrAutoType, - onError: PropTypes.func, - showLegend: PropTypes.bool, - showMarkers: PropTypes.bool, - vizType: PropTypes.oneOf([ - VizType.BoxPlot, - 'bubble', - VizType.Bullet, - 'column', - VizType.TimePivot, - 'pie', - ]), - xAxisFormat: PropTypes.string, - numberFormat: PropTypes.string, - xAxisLabel: PropTypes.string, - xAxisShowMinMax: PropTypes.bool, - xIsLogScale: PropTypes.bool, - xTicksLayout: PropTypes.oneOf(['auto', 'staggered', '45°']), - yAxisFormat: PropTypes.string, - yAxisBounds: PropTypes.arrayOf(PropTypes.number), - yAxisLabel: PropTypes.string, - yAxisShowMinMax: PropTypes.bool, - yIsLogScale: PropTypes.bool, - // 'bar' or 'dist-bar' - isBarStacked: PropTypes.bool, - showBarValue: PropTypes.bool, - // 'line' only - showBrush: PropTypes.oneOf([true, 'yes', false, 'no', 'auto']), - onBrushEnd: PropTypes.func, - // 'line-multi' or 'dual-line' - yAxis2Format: PropTypes.string, - // 'line', 'time-pivot', 'dual-line' or 'line-multi' - lineInterpolation: PropTypes.string, - // 'pie' only - isDonut: PropTypes.bool, - isPieLabelOutside: PropTypes.bool, - pieLabelType: PropTypes.oneOf([ - 'key', - 'value', - 'percent', - 'key_value', - 'key_percent', - 'key_value_percent', - ]), - showLabels: PropTypes.bool, - // 'bubble' only - entity: PropTypes.string, - maxBubbleSize: PropTypes.number, - xField: stringOrObjectWithLabelType, - yField: stringOrObjectWithLabelType, - sizeField: stringOrObjectWithLabelType, - // time-pivot only - baseColor: rgbObjectType, -}; - -const NOOP = () => {}; - -function nvd3Vis(element, props) { - const { - data, - width: maxWidth, - height: maxHeight, - annotationData, - annotationLayers = [], - baseColor, - bottomMargin, - colorScheme, - comparisonType, - contribution, - isBarStacked, - isDonut, - isPieLabelOutside, - leftMargin, - lineInterpolation = 'linear', - markerLabels, - markerLines, - markerLineLabels, - markers, - onBrushEnd = NOOP, - onError = NOOP, - pieLabelType, - rangeLabels, - ranges, - showBarValue, - showBrush, - showLabels, - showLegend, - showMarkers, - vizType, - xAxisFormat, - numberFormat, - xAxisLabel, - xAxisShowMinMax = false, - xIsLogScale, - xTicksLayout, - yAxisFormat, - yAxisBounds, - yAxisLabel, - yAxisShowMinMax = false, - yAxis2ShowMinMax = false, - yIsLogScale, - sliceId, - } = props; - - const isExplore = document.querySelector('#explorer-container') !== null; - const container = element; - container.innerHTML = ''; - const activeAnnotationLayers = annotationLayers.filter(layer => layer.show); - - // Search for the chart id in a parent div from the nvd3 chart - let chartContainer = container; - let chartId = null; - while (chartContainer.parentElement) { - if (chartContainer.parentElement.id.startsWith(CHART_ID_PREFIX)) { - chartId = chartContainer.parentElement.id; - break; - } - - chartContainer = chartContainer.parentElement; - } - - let chart; - const width = maxWidth; - let colorKey = 'key'; - - container.style.width = `${maxWidth}px`; - container.style.height = `${maxHeight}px`; - - function isVizTypes(types) { - return types.includes(vizType); - } - - const drawGraph = function drawGraph() { - const d3Element = d3.select(element); - d3Element.classed('superset-legacy-chart-nvd3', true); - d3Element.classed(`superset-legacy-chart-nvd3-${kebabCase(vizType)}`, true); - let svg = d3Element.select('svg'); - if (svg.empty()) { - svg = d3Element.append('svg'); - } - const height = - vizType === VizType.Bullet ? Math.min(maxHeight, 50) : maxHeight; - const isTimeSeries = isVizTypes(TIMESERIES_VIZ_TYPES); - - // Handling xAxis ticks settings - const staggerLabels = xTicksLayout === 'staggered'; - const xLabelRotation = xTicksLayout === '45°' ? 45 : 0; - if (xLabelRotation === 45 && isTruthy(showBrush)) { - onError( - t('You cannot use 45° tick layout along with the time range filter'), - ); - - return null; - } - - const canShowBrush = - isTruthy(showBrush) || - (showBrush === 'auto' && - maxHeight >= MIN_HEIGHT_FOR_BRUSH && - xTicksLayout !== '45°'); - const numberFormatter = getNumberFormatter(numberFormat); - - switch (vizType) { - case VizType.TimePivot: - chart = nv.models.lineChart(); - chart.xScale(d3.time.scale.utc()); - chart.interpolate(lineInterpolation); - break; - - case VizType.Pie: - chart = nv.models.pieChart(); - colorKey = 'x'; - chart.valueFormat(numberFormatter); - if (isDonut) { - chart.donut(true); - } - chart.showLabels(showLabels); - chart.labelsOutside(isPieLabelOutside); - // Configure the minimum slice size for labels to show up - chart.labelThreshold(0.05); - chart.cornerRadius(true); - - if (['key', 'value', 'percent'].includes(pieLabelType)) { - chart.labelType(pieLabelType); - } else if (pieLabelType === 'key_value') { - chart.labelType(d => `${d.data.x}: ${numberFormatter(d.data.y)}`); - } else { - // pieLabelType in ['key_percent', 'key_value_percent'] - const total = d3.sum(data, d => d.y); - const percentFormatter = getNumberFormatter( - NumberFormats.PERCENT_2_POINT, - ); - if (pieLabelType === 'key_percent') { - chart.tooltip.valueFormatter(d => percentFormatter(d)); - chart.labelType( - d => `${d.data.x}: ${percentFormatter(d.data.y / total)}`, - ); - } else { - // pieLabelType === 'key_value_percent' - chart.tooltip.valueFormatter( - d => `${numberFormatter(d)} (${percentFormatter(d / total)})`, - ); - chart.labelType( - d => - `${d.data.x}: ${numberFormatter(d.data.y)} (${percentFormatter( - d.data.y / total, - )})`, - ); - } - } - // Pie chart does not need top margin - chart.margin({ top: 0 }); - break; - - case 'column': - chart = nv.models.multiBarChart().reduceXTicks(false); - break; - - case VizType.BoxPlot: - colorKey = 'label'; - chart = nv.models.boxPlotChart(); - chart.x(d => d.label); - chart.maxBoxWidth(75); // prevent boxes from being incredibly wide - break; - - case VizType.Bullet: - chart = nv.models.bulletChart(); - data.rangeLabels = rangeLabels; - data.ranges = ranges; - data.markerLabels = markerLabels; - data.markerLines = markerLines; - data.markerLineLabels = markerLineLabels; - data.markers = markers; - break; - - default: - throw new Error(`Unrecognized visualization for nvd3${vizType}`); - } - // Assuming the container has padding already other than for top margin - chart.margin({ left: 0, bottom: 0 }); - - if (showBarValue) { - drawBarValues(svg, data, isBarStacked, yAxisFormat); - chart.dispatch.on('stateChange.drawBarValues', () => { - drawBarValues(svg, data, isBarStacked, yAxisFormat); - }); - } - - if (canShowBrush && onBrushEnd !== NOOP) { - if (chart.focus) { - chart.focus.dispatch.on('brush', event => { - const timeRange = stringifyTimeRange(event.extent); - if (timeRange) { - event.brush.on('brushend', () => { - onBrushEnd(timeRange); - }); - } - }); - } - } - - if (chart.xAxis && chart.xAxis.staggerLabels) { - chart.xAxis.staggerLabels(staggerLabels); - } - if (chart.xAxis && chart.xAxis.rotateLabels) { - chart.xAxis.rotateLabels(xLabelRotation); - } - if (chart.x2Axis && chart.x2Axis.staggerLabels) { - chart.x2Axis.staggerLabels(staggerLabels); - } - if (chart.x2Axis && chart.x2Axis.rotateLabels) { - chart.x2Axis.rotateLabels(xLabelRotation); - } - - if ('showLegend' in chart && typeof showLegend !== 'undefined') { - if (width < BREAKPOINTS.small && vizType !== VizType.Pie) { - chart.showLegend(false); - } else { - chart.showLegend(showLegend); - } - } - - if (yIsLogScale) { - chart.yScale(d3.scale.log()); - } - if (xIsLogScale) { - chart.xScale(d3.scale.log()); - } - - let xAxisFormatter; - // Time Pivot's x-axis is the elapsed offset within the period, not a - // timestamp, so it defaults x_axis_format to a number format (SMART_NUMBER). - // Running that through getTimeFormatter makes d3 treat "SMART_NUMBER" as a - // literal template and print it for every tick, so format it as a number. - if (isTimeSeries && vizType !== VizType.TimePivot) { - xAxisFormatter = getTimeFormatter(xAxisFormat); - // In tooltips, always use the verbose time format - chart.interactiveLayer.tooltip.headerFormatter(smartDateVerboseFormatter); - } else { - xAxisFormatter = getTimeOrNumberFormatter(xAxisFormat); - } - if (chart.x2Axis && chart.x2Axis.tickFormat) { - chart.x2Axis.tickFormat(xAxisFormatter); - } - if (chart.xAxis && chart.xAxis.tickFormat) { - const isXAxisString = isVizTypes([VizType.BoxPlot]); - if (isXAxisString) { - chart.xAxis.tickFormat(d => - d.length > MAX_NO_CHARACTERS_IN_LABEL - ? `${d.slice(0, Math.max(0, MAX_NO_CHARACTERS_IN_LABEL))}…` - : d, - ); - } else { - chart.xAxis.tickFormat(xAxisFormatter); - } - } - - let yAxisFormatter = getTimeOrNumberFormatter(yAxisFormat); - if (chart.yAxis && chart.yAxis.tickFormat) { - if ( - (contribution || comparisonType === 'percentage') && - (!yAxisFormat || - yAxisFormat === NumberFormats.SMART_NUMBER || - yAxisFormat === NumberFormats.SMART_NUMBER_SIGNED) - ) { - // When computing a "Percentage" or "Contribution" selected, - // force a percentage format if no custom formatting set - yAxisFormatter = getNumberFormatter(NumberFormats.PERCENT_1_POINT); - } - chart.yAxis.tickFormat(yAxisFormatter); - } - if (chart.y2Axis && chart.y2Axis.tickFormat) { - chart.y2Axis.tickFormat(yAxisFormatter); - } - - if (chart.yAxis) { - chart.yAxis.ticks(5); - } - if (chart.y2Axis) { - chart.y2Axis.ticks(5); - } - - // Set showMaxMin for all axis - setAxisShowMaxMin(chart.xAxis, xAxisShowMinMax); - setAxisShowMaxMin(chart.x2Axis, xAxisShowMinMax); - setAxisShowMaxMin(chart.yAxis, yAxisShowMinMax); - setAxisShowMaxMin(chart.y2Axis, yAxis2ShowMinMax || yAxisShowMinMax); - - if (vizType === VizType.TimePivot) { - if (baseColor) { - const { r, g, b } = baseColor; - chart.color(d => { - const alpha = d.rank > 0 ? d.perc * 0.5 : 1; - - return `rgba(${r}, ${g}, ${b}, ${alpha})`; - }); - } - - chart.useInteractiveGuideline(true); - chart.interactiveLayer.tooltip.contentGenerator(d => - generateTimePivotTooltip(d, xAxisFormatter, yAxisFormatter), - ); - } else if (vizType !== VizType.Bullet) { - const colorFn = getScale(colorScheme); - chart.color( - d => d.color || colorFn(cleanColorInput(d[colorKey]), sliceId), - ); - } - - // This is needed for correct chart dimensions if a chart is rendered in a hidden container - chart.width(width); - chart.height(height); - - svg - .datum(data) - .transition() - .duration(500) - .attr('height', height) - .attr('width', width) - .call(chart); - - // For log scale, only show 1, 10, 100, 1000, ... - if (yIsLogScale) { - chart.yAxis.tickFormat(d => - d !== 0 && Math.log10(d) % 1 === 0 ? yAxisFormatter(d) : '', - ); - } - - if (xLabelRotation > 0) { - // shift labels to the left so they look better - const xTicks = svg.select('.nv-x.nv-axis > g').selectAll('g'); - xTicks.selectAll('text').attr('dx', -6.5); - } - - const applyYAxisBounds = () => { - if ( - chart.yDomain && - Array.isArray(yAxisBounds) && - yAxisBounds.length === 2 - ) { - const [customMin, customMax] = yAxisBounds; - const hasCustomMin = isDefined(customMin) && !Number.isNaN(customMin); - const hasCustomMax = isDefined(customMax) && !Number.isNaN(customMax); - - if (hasCustomMin && hasCustomMax) { - // Override the y domain if there's both a custom min and max - chart.yDomain([customMin, customMax]); - chart.clipEdge(true); - } else if (hasCustomMin || hasCustomMax) { - // Only one of the bounds has been set, so we need to manually calculate the other one - const [trueMin, trueMax] = computeYDomain(data); - const min = hasCustomMin ? customMin : trueMin; - const max = hasCustomMax ? customMax : trueMax; - chart.yDomain([min, max]); - chart.clipEdge(true); - } - } - }; - applyYAxisBounds(); - - // Also reapply on each state change to account for enabled/disabled series - if (chart.dispatch && chart.dispatch.stateChange) { - chart.dispatch.on('stateChange.applyYAxisBounds', applyYAxisBounds); - } - - if (showMarkers) { - svg - .selectAll('.nv-point') - .style('stroke-opacity', 1) - .style('fill-opacity', 1); - - // redo on legend toggle; nvd3 calls the callback *before* the line is - // drawn, so we need to add a small delay here - chart.dispatch.on('stateChange.showMarkers', () => { - setTimeout(() => { - svg - .selectAll('.nv-point') - .style('stroke-opacity', 1) - .style('fill-opacity', 1); - }, 10); - }); - } - - if (chart.yAxis !== undefined || chart.yAxis2 !== undefined) { - // Hack to adjust y axis left margin to accommodate long numbers - const marginPad = Math.ceil( - Math.min(maxWidth * (isExplore ? 0.01 : 0.03), MAX_MARGIN_PAD), - ); - // Hack to adjust margins to accommodate long axis tick labels. - // - has to be done only after the chart has been rendered once - // - measure the width or height of the labels - // ---- (x axis labels are rotated 45 degrees so we use height), - // - adjust margins based on these measures and render again - const margins = chart.margin(); - if (chart.xAxis) { - margins.bottom = 28; - } - const maxYAxisLabelWidth = getMaxLabelSize( - svg, - chart.yAxis2 ? 'nv-y1' : 'nv-y', - ); - const maxXAxisLabelHeight = getMaxLabelSize(svg, 'nv-x'); - margins.left = maxYAxisLabelWidth + marginPad; - - if (yAxisLabel && yAxisLabel !== '') { - margins.left += 25; - } - if (showBarValue) { - // Add more margin to avoid label colliding with legend. - margins.top += 24; - } - if (xAxisShowMinMax) { - // If x bounds are shown, we need a right margin - margins.right = Math.max(20, maxXAxisLabelHeight / 2) + marginPad; - } - if (xLabelRotation === 45) { - margins.bottom = - maxXAxisLabelHeight * Math.sin((Math.PI * xLabelRotation) / 180) + - marginPad + - 30; - margins.right = - maxXAxisLabelHeight * Math.cos((Math.PI * xLabelRotation) / 180) + - marginPad; - } else if (staggerLabels) { - margins.bottom = 40; - } - - if (bottomMargin && bottomMargin !== 'auto') { - margins.bottom = parseInt(bottomMargin, 10); - } - if (leftMargin && leftMargin !== 'auto') { - margins.left = leftMargin; - } - - if (xAxisLabel && xAxisLabel !== '' && chart.xAxis) { - margins.bottom += 25; - let distance = 0; - if (margins.bottom && !Number.isNaN(margins.bottom)) { - distance = margins.bottom - 45; - } - // nvd3 bug axisLabelDistance is disregarded on xAxis - // https://github.com/krispo/angular-nvd3/issues/90 - chart.xAxis.axisLabel(xAxisLabel).axisLabelDistance(distance); - } - - if (yAxisLabel && yAxisLabel !== '' && chart.yAxis) { - let distance = 0; - if (margins.left && !Number.isNaN(margins.left)) { - distance = margins.left - 70; - } - chart.yAxis.axisLabel(yAxisLabel).axisLabelDistance(distance); - } - if (isTimeSeries && annotationData && activeAnnotationLayers.length > 0) { - // Time series annotations add additional data - const timeSeriesAnnotations = activeAnnotationLayers - .filter( - layer => layer.annotationType === ANNOTATION_TYPES.TIME_SERIES, - ) - .reduce( - (bushel, a) => - bushel.concat( - (annotationData[a.name] || []).map(series => { - if (!series) { - return {}; - } - const key = Array.isArray(series.key) - ? `${a.name}, ${series.key.join(', ')}` - : `${a.name}, ${series.key}`; - - return { - ...series, - key, - color: a.color, - strokeWidth: a.width, - classed: `${a.opacity} ${a.style} nv-timeseries-annotation-layer showMarkers${a.showMarkers} hideLine${a.hideLine}`, - }; - }), - ), - [], - ); - data.push(...timeSeriesAnnotations); - } - - // Uniquely identify tooltips based on chartId so this chart instance only - // controls its own tooltips - if (chartId) { - if (chart && chart.interactiveLayer && chart.interactiveLayer.tooltip) { - chart.interactiveLayer.tooltip.classes([ - generateTooltipClassName(chartId), - ]); - } - - if (chart && chart.tooltip) { - chart.tooltip.classes([generateTooltipClassName(chartId)]); - } - } - - // render chart - chart.margin(margins); - svg - .datum(data) - .transition() - .duration(500) - .attr('width', width) - .attr('height', height) - .call(chart); - - // On scroll, hide (not remove) tooltips so they can reappear on hover. - // Throttle to only 4x/second. - window.addEventListener( - 'scroll', - throttle(() => hideTooltips(false), 250), - ); - - // The below code should be run AFTER rendering because chart is updated in call() - if (isTimeSeries && activeAnnotationLayers.length > 0) { - // Formula annotations - const formulas = activeAnnotationLayers.filter( - a => a.annotationType === ANNOTATION_TYPES.FORMULA, - ); - - const xMax = chart.xAxis.scale().domain()[1].valueOf(); - const xMin = chart.xAxis.scale().domain()[0].valueOf(); - let xScale; - if (chart.xScale) { - xScale = chart.xScale(); - } else if (chart.xAxis.scale) { - xScale = chart.xAxis.scale(); - } else { - xScale = d3.scale.linear(); - } - if (xScale && xScale.clamp) { - xScale.clamp(true); - } - - if (formulas.length > 0) { - const xValues = []; - let period = Math.min( - ...data.map(d => - Math.min(...d.values.slice(1).map((v, i) => v.x - d.values[i].x)), - ), - ); - const dataPoints = (xMax - xMin) / (period || 1); - // make sure that there are enough data points and not too many - period = dataPoints < 100 ? (xMax - xMin) / 100 : period; - period = dataPoints > 500 ? (xMax - xMin) / 500 : period; - xValues.push(xMin); - for (let x = xMin; x < xMax; x += period) { - xValues.push(x); - } - xValues.push(xMax); - - const formulaData = formulas.map(fo => { - const { value: expression } = fo; - return { - key: fo.name, - values: xValues.map(x => ({ - x, - y: evalExpression(expression, x), - })), - color: fo.color, - strokeWidth: fo.width, - classed: `${fo.opacity} ${fo.style}`, - }; - }); - data.push(...formulaData); - } - const xAxis = chart.xAxis1 ? chart.xAxis1 : chart.xAxis; - const yAxis = chart.yAxis1 ? chart.yAxis1 : chart.yAxis; - const chartWidth = xAxis.scale().range()[1]; - const annotationHeight = yAxis.scale().range()[0]; - - if (annotationData) { - // Event annotations - activeAnnotationLayers - .filter( - x => - x.annotationType === ANNOTATION_TYPES.EVENT && - annotationData && - annotationData[x.name], - ) - .forEach((config, index) => { - const e = applyNativeColumns(config); - // Add event annotation layer - const annotations = d3 - .select(element) - .select('.nv-wrap') - .append('g') - .attr('class', `nv-event-annotation-layer-${index}`); - const aColor = - e.color || getColor(cleanColorInput(e.name), colorScheme); - - const tip = tipFactory({ - ...e, - annotationTipClass: `arrow-down nv-event-annotation-layer-${config.sourceType}`, - }); - const records = (annotationData[e.name].records || []) - .map(r => { - const timeValue = new Date(dayjs.utc(r[e.timeColumn])); - - return { - ...r, - [e.timeColumn]: timeValue, - }; - }) - .filter( - record => - !Number.isNaN(record[e.timeColumn].getMilliseconds()), - ); - - if (records.length > 0) { - annotations - .selectAll('line') - .data(records) - .enter() - .append('line') - .attr({ - x1: d => xScale(new Date(d[e.timeColumn])), - y1: 0, - x2: d => xScale(new Date(d[e.timeColumn])), - y2: annotationHeight, - }) - .attr('class', `${e.opacity} ${e.style}`) - .style('stroke', aColor) - .style('stroke-width', e.width) - .on('mouseover', tip.show) - .on('mouseout', tip.hide) - .call(tip); - } - - // update annotation positions on brush event - if (chart.focus) { - chart.focus.dispatch.on('onBrush.event-annotation', () => { - annotations - .selectAll('line') - .data(records) - .attr({ - x1: d => xScale(new Date(d[e.timeColumn])), - y1: 0, - x2: d => xScale(new Date(d[e.timeColumn])), - y2: annotationHeight, - opacity: d => { - const x = xScale(new Date(d[e.timeColumn])); - - return x > 0 && x < chartWidth ? 1 : 0; - }, - }); - }); - } - }); - - // Interval annotations - activeAnnotationLayers - .filter( - x => - x.annotationType === ANNOTATION_TYPES.INTERVAL && - annotationData && - annotationData[x.name], - ) - .forEach((config, index) => { - const e = applyNativeColumns(config); - // Add interval annotation layer - const annotations = d3 - .select(element) - .select('.nv-wrap') - .append('g') - .attr('class', `nv-interval-annotation-layer-${index}`); - - const aColor = - e.color || getColor(cleanColorInput(e.name), colorScheme); - const tip = tipFactory(e); - - const records = (annotationData[e.name].records || []) - .map(r => { - const timeValue = new Date(dayjs.utc(r[e.timeColumn])); - const intervalEndValue = new Date( - dayjs.utc(r[e.intervalEndColumn]), - ); - - return { - ...r, - [e.timeColumn]: timeValue, - [e.intervalEndColumn]: intervalEndValue, - }; - }) - .filter( - record => - !Number.isNaN(record[e.timeColumn].getMilliseconds()) && - !Number.isNaN( - record[e.intervalEndColumn].getMilliseconds(), - ), - ); - - if (records.length > 0) { - annotations - .selectAll('rect') - .data(records) - .enter() - .append('rect') - .attr({ - x: d => - Math.min( - xScale(new Date(d[e.timeColumn])), - xScale(new Date(d[e.intervalEndColumn])), - ), - y: 0, - width: d => - Math.max( - Math.abs( - xScale(new Date(d[e.intervalEndColumn])) - - xScale(new Date(d[e.timeColumn])), - ), - 1, - ), - height: annotationHeight, - }) - .attr('class', `${e.opacity} ${e.style}`) - .style('stroke-width', e.width) - .style('stroke', aColor) - .style('fill', aColor) - .style('fill-opacity', 0.2) - .on('mouseover', tip.show) - .on('mouseout', tip.hide) - .call(tip); - } - - // update annotation positions on brush event - if (chart.focus) { - chart.focus.dispatch.on('onBrush.interval-annotation', () => { - annotations - .selectAll('rect') - .data(records) - .attr({ - x: d => xScale(new Date(d[e.timeColumn])), - width: d => { - const x1 = xScale(new Date(d[e.timeColumn])); - const x2 = xScale(new Date(d[e.intervalEndColumn])); - - return x2 - x1; - }, - }); - }); - } - }); - } - - // rerender chart appended with annotation layer - svg.datum(data).attr('height', height).attr('width', width).call(chart); - - // Display styles for Time Series Annotations - chart.dispatch.on('renderEnd.timeseries-annotation', () => { - d3.selectAll( - '.slice_container .nv-timeseries-annotation-layer.showMarkerstrue .nv-point', - ) - .style('stroke-opacity', 1) - .style('fill-opacity', 1); - d3.selectAll( - '.slice_container .nv-timeseries-annotation-layer.hideLinetrue', - ).style('stroke-width', 0); - }); - } - } - - wrapTooltip(chart); - - return chart; - }; - - // Remove tooltips before rendering chart, if the chart is being re-rendered sometimes - // there are left over tooltips in the dom, - // this will clear them before rendering the chart again. - if (chartId) { - removeTooltip(chartId); - } else { - hideTooltips(true); - } - - nv.addGraph(drawGraph); -} - -nvd3Vis.displayName = 'NVD3'; -nvd3Vis.propTypes = propTypes; -export default nvd3Vis; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts b/superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts deleted file mode 100644 index 99bf998f4020..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/PropTypes.ts +++ /dev/null @@ -1,82 +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 react/sort-prop-types */ -import PropTypes from 'prop-types'; -import { ANNOTATION_TYPES } from './vendor/superset/AnnotationTypes'; - -export const numberOrAutoType = PropTypes.oneOfType([ - PropTypes.number, - PropTypes.oneOf(['auto']), -]); - -export const stringOrObjectWithLabelType = PropTypes.oneOfType([ - PropTypes.string, - PropTypes.shape({ - label: PropTypes.string, - }), -]); - -export const rgbObjectType = PropTypes.shape({ - r: PropTypes.number.isRequired, - g: PropTypes.number.isRequired, - b: PropTypes.number.isRequired, -}); - -export const numericXYType = PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number, -}); - -export const categoryAndValueXYType = PropTypes.shape({ - x: PropTypes.string, - y: PropTypes.number, -}); - -export const boxPlotValueType = PropTypes.shape({ - outliers: PropTypes.arrayOf(PropTypes.number), - Q1: PropTypes.number, - Q2: PropTypes.number, - Q3: PropTypes.number, - whisker_high: PropTypes.number, - whisker_low: PropTypes.number, -}); - -export const bulletDataType = PropTypes.shape({ - markerLabels: PropTypes.arrayOf(PropTypes.string), - markerLineLabels: PropTypes.arrayOf(PropTypes.string), - markerLines: PropTypes.arrayOf(PropTypes.number), - markers: PropTypes.arrayOf(PropTypes.number), - measures: PropTypes.arrayOf(PropTypes.number), - rangeLabels: PropTypes.arrayOf(PropTypes.string), - ranges: PropTypes.arrayOf(PropTypes.number), -}); - -export const annotationLayerType = PropTypes.shape({ - annotationType: PropTypes.oneOf(Object.keys(ANNOTATION_TYPES)), - color: PropTypes.string, - hideLine: PropTypes.bool, - name: PropTypes.string, - opacity: PropTypes.string, - show: PropTypes.bool, - showMarkers: PropTypes.bool, - sourceType: PropTypes.string, - style: PropTypes.string, - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - width: PropTypes.number, -}); diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx b/superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx deleted file mode 100644 index f472ac245289..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/ReactNVD3.tsx +++ /dev/null @@ -1,189 +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. - */ -// @ts-nocheck -- legacy reactified component with untyped `this` context from reactify callbacks -import { reactify } from '@superset-ui/core'; -import { styled } from '@apache-superset/core/theme'; -import PropTypes from 'prop-types'; -import Component from './NVD3Vis'; -import { hideTooltips, removeTooltip } from './utils'; - -function componentWillUnmount() { - const { id } = this.props; - if (id !== null && id !== undefined) { - removeTooltip(id); - } else { - hideTooltips(true); - } -} - -const ReactNVD3 = reactify(Component, { componentWillUnmount }); - -const NVD3 = ({ className, ...otherProps }) => ( -

-); - -NVD3.propTypes = { - className: PropTypes.string.isRequired, -}; - -export default styled(NVD3)` - .superset-legacy-chart-nvd3-dist-bar, - .superset-legacy-chart-nvd3-bar { - overflow-x: auto !important; - svg { - &.nvd3-svg { - width: auto; - font-size: ${({ theme }) => theme.fontSize}; - } - } - } - .superset-legacy-chart-nvd3 { - nv-x text { - font-size: ${({ theme }) => theme.fontSize}; - } - g.superset path { - stroke-dasharray: 5, 5; - } - .nvtooltip { - table td { - font-size: @font-size-s !important; - } - } - .nvtooltip tr.highlight td { - font-weight: ${({ theme }) => theme.fontWeightStrong}; - font-size: ${({ theme }) => theme.fontSize}px !important; - } - text.nv-axislabel { - font-size: ${({ theme }) => theme.fontSize} !important; - } - g.nv-axis text { - fill: ${({ theme }) => theme.colorText}; - } - g.nv-series text { - fill: ${({ theme }) => theme.colorText}; - } - g.solid path, - line.solid { - stroke-dasharray: unset; - } - g.dashed path, - line.dashed { - stroke-dasharray: 5, 5; - } - g.longDashed path, - line.dotted { - stroke-dasharray: 1, 1; - } - - g.opacityLow path, - line.opacityLow { - stroke-opacity: 0.2; - } - - g.opacityMedium path, - line.opacityMedium { - stroke-opacity: 0.5; - } - g.opacityHigh path, - line.opacityHigh { - stroke-opacity: 0.8; - } - g.time-shift-0 path, - line.time-shift-0 { - stroke-dasharray: 5, 5; - } - g.time-shift-1 path, - line.time-shift-1 { - stroke-dasharray: 1, 5; - } - g.time-shift-2 path, - line.time-shift-3 { - stroke-dasharray: 5, 1; - } - g.time-shift-3 path, - line.time-shift-3 { - stroke-dasharray: 5, 1; - } - g.time-shift-4 path, - line.time-shift-4 { - stroke-dasharray: 5, 10; - } - g.time-shift-5 path, - line.time-shift-5 { - stroke-dasharray: 0.9; - } - g.time-shift-6 path, - line.time-shift-6 { - stroke-dasharray: 15, 10, 5; - } - g.time-shift-7 path, - line.time-shift-7 { - stroke-dasharray: 15, 10, 5, 10; - } - g.time-shift-8 path, - line.time-shift-8 { - stroke-dasharray: 15, 10, 5, 10, 15; - } - g.time-shift-9 path, - line.time-shift-9 { - stroke-dasharray: 5, 5, 1, 5; - } - .nv-noData.body { - font-size: ${({ theme }) => theme.fontSize}; - font-weight: ${({ theme }) => theme.fontWeightNormal}; - } - } - .superset-legacy-chart-nvd3-tr-highlight { - border-top: 1px solid; - border-bottom: 1px solid; - font-weight: ${({ theme }) => theme.fontWeightStrong}; - } - .superset-legacy-chart-nvd3-tr-total { - font-weight: ${({ theme }) => theme.fontWeightStrong}; - } - .nvtooltip { - .tooltip-header { - white-space: nowrap; - font-weight: ${({ theme }) => theme.fontWeightStrong}; - } - tbody tr:not(.tooltip-header) td:nth-of-type(2) { - word-break: break-word; - } - } - .d3-tip.nv-event-annotation-layer-table, - .d3-tip.nv-event-annotation-layer-NATIVE { - width: 200px; - border-radius: 2px; - background-color: ${({ theme }) => theme.colorBgContainer}; - fill-opacity: 0.6; - margin: ${({ theme }) => theme.sizeUnit * 2}px; - padding: ${({ theme }) => theme.sizeUnit * 2}px; - color: ${({ theme }) => theme.colorTextLightSolid}; - &:after { - content: '\\25BC'; - font-size: ${({ theme }) => theme.fontSize}; - color: ${({ theme }) => theme.colorText}; - position: absolute; - bottom: -14px; - left: 94px; - } - } -`; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts deleted file mode 100644 index 76a788b6e694..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/buildQuery.ts +++ /dev/null @@ -1,34 +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 { buildQueryContext, QueryFormData } from '@superset-ui/core'; - -/** - * Mirrors the legacy NVD3TimePivotViz.query_obj: a timeseries query with - * the single metric. - */ -export default function buildQuery(formData: QueryFormData) { - const { metric } = formData; - return buildQueryContext(formData, baseQueryObject => [ - { - ...baseQueryObject, - is_timeseries: true, - metrics: metric ? [metric] : [], - }, - ]); -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts deleted file mode 100644 index 1a338a91e634..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/controlPanel.ts +++ /dev/null @@ -1,134 +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 { t } from '@apache-superset/core/translation'; -import { QueryFormData } from '@superset-ui/core'; -import { - ControlPanelConfig, - D3_FORMAT_OPTIONS, - getStandardizedControls, - sections, -} from '@superset-ui/chart-controls'; -import { - lineInterpolation, - showLegend, - xAxisLabel, - bottomMargin, - xAxisFormat, - yLogScale, - yAxisBounds, - xAxisShowMinmax, - yAxisShowMinmax, - yAxisLabel, - leftMargin, -} from '../NVD3Controls'; - -const config: ControlPanelConfig = { - controlPanelSections: [ - sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - ['metric'], - ['adhoc_filters'], - [ - { - name: 'freq', - config: { - type: 'SelectControl', - label: t('Frequency'), - default: 'W-MON', - freeForm: true, - clearable: false, - choices: [ - ['AS', t('Year (freq=AS)')], - ['52W-MON', t('52 weeks starting Monday (freq=52W-MON)')], - ['W-SUN', t('1 week starting Sunday (freq=W-SUN)')], - ['W-MON', t('1 week starting Monday (freq=W-MON)')], - ['D', t('Day (freq=D)')], - ['4W-MON', t('4 weeks (freq=4W-MON)')], - ], - description: t( - `The periodicity over which to pivot time. Users can provide - "Pandas" offset alias. - Click on the info bubble for more details on accepted "freq" expressions.`, - ), - tooltipOnClick: () => { - window.open( - 'https://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases', - ); - }, - }, - }, - ], - ], - }, - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [showLegend], - [lineInterpolation], - ['color_picker', null], - ], - }, - { - label: t('X Axis'), - expanded: true, - controlSetRows: [ - [xAxisLabel], - [bottomMargin], - [xAxisShowMinmax], - [ - { - name: xAxisFormat.name, - config: { - ...xAxisFormat.config, - default: 'SMART_NUMBER', - choices: D3_FORMAT_OPTIONS, - }, - }, - ], - ], - }, - { - label: t('Y Axis'), - expanded: true, - controlSetRows: [ - [yAxisLabel], - [leftMargin], - [yAxisShowMinmax], - [yLogScale], - ['y_axis_format'], - [yAxisBounds], - ], - }, - ], - controlOverrides: { - metric: { - clearable: false, - }, - }, - formDataOverrides: (formData: QueryFormData) => ({ - ...formData, - metric: getStandardizedControls().shiftMetric(), - }), -}; - -export default config; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example-dark.jpg b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/example-dark.jpg deleted file mode 100644 index 6d92e0682b4fef33d1c86f21a1b36d76eae0ee68..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137758 zcmeFZ1yo#JmNt9|1P=rP!6kUG1eX8>A$V{Lo*==3yB3fj!6lFvhu|Kha7*wI+=5fM zQ-u~#6o0)v-96LYGxPWK%v#@??`v3RQFXcZp1aT8XYc3Pd!KXr>vjdWuPm=54`5(m z04eBS;C3F61#q#kaj@^=;^5%m;o;&FlHDUDARwe7eLze`N6kP_M@>u1$imCc$i&S| zOUohhnENT8pr9ZFyO^XXzXY#G#)BzH+4@yTF6(0qf#78RG2*3{NP>KhuHn!k5-_w@Gl5BwOPn4FrP`87KSU0qw>*xcIQ z*@YjUoSvOuATFtQEZJYlMS_;=4i*+B7S8W-VchXTH%t<&yN~#=No6!~ z-ncwq;t$3pd!CS6{SA*FBwIBBqHU|XlV!p)<>}fbq^F?Ow|QbO`Z!$AkzcD*h^19LQmnQ~nN zTle3+RIi+#YUc1==s|mUiHgk+V|BXx=uidiMndx-{^Bo@0LLF1#8E<^YR&+%F&thZ zYnLOS&YB+?ME{|hvXAMHr;YX<|Vg1Pf z7Q9ufw})OttGry4@%`v1=D*zfPhAw%)izJ^CQkpDvrqphP=g+jdTcdd9d-|pku#li zH_=x(z6I*ggYrZ~%53hn&BYp3?}|Wl+)rKVKR#r|&Xs9y*~`(LdJ6r-(j&|I(EQg; zy^0#%>w)eVT9FWN;j*n4gZ1RQ%6D6wG=q&F^WM>i6Jqp{>;3&Yz^TZXw!8Yw&pSWO zmXb+p*z6cA&p+@;2@&F`++HB`O%!wzF-E4t8+|_PIiq;-m9+{tdLsB{44k!! zn|-X?RhH5-Y@NG!TTOgzhSs9Z{R)-&rVOFC*`?DYJgxn_jQhLX^Y@HfZg+4PJ<8Q_ zyxC=7p#@X7KzN#iyio*<#qWdbiW}q=ke3F)sv=QTHL!*l(;0Pj-tpDDO|jhT3W=S; z2g&U4N^6BtA*cJsl!BR3eoYi9$4QR@i*k_m>V0#E1*^oAri*qdh2yKj$9GHCzn6`< zEjNw$&y2S-z>rx~FhOz8@O#A__puk9mGhJC^AGEg%7dkG$S_WlFS;10cBU)bTyhqC zl=)w(CB{odbfc0-Yz=h}A*C{fJFt(ATdWq*&4sHNqJ#IJ`OZu(QPsn5+6%Y3KR5GC zy)Ws6EY&c3_Vr`XH&wABx(bUz{R4{Zn+?lJjm@|4pGvGd8qwbZya6YB(zNUmt#nlg z4Wmpab>S~-UZ4CYo#$P$o7Kc)Aq?TIk1G(RKDlG>R3|!f^XbhQHPmpprK8%aGI)k6=e#gPE=;Bsb-4KV!jRl;1Ef`H(i4ogNhr zG+^gNZ0V)aj4jc3;2+7)*DWnem~+V?g-Bs|Y{k2VhiT;BK8EQQSVcLXVe}7q+yaCZ z7Vy3vIt7y{X3~fTlXCTbqE}c9a&(|Y^1XJ>2Kdm>f(q2HjedMepZCIVPX66#?{}bA zavO8wz^Ti3B%JLPBtYCx-l^HrEV(^e6ck({Aqh3H@6jH@j9xHD(*-b}NgQ%+zdyla`f zzLqyWjeEoozf3V=`$uq?yfIxK$grf!ThI9M!^vXaHYTqUT3U?x9Mlo{o*bB1M^S@Fk+jY2O&?{lRg6~h5VVZ(7Vy;| zDHqt$2cczh$oK6Okex8}Wk=hK`4-XZp8}(&rG~w6;2gVJ+q4XNx%FrM{TF2pKf-9y zBTKh8a|>9-t+@Sb4SmDR@A88hj9riii#3F!2|KTct$I4fQ71s=xZs>s0r` zIUI`?d&Fg~sm#x(vPh8!nrNH(>c?y}oP3u4jZGjdMab!Ty&kN*addh;#0x)o za`3jkQN4|}ST}sde&^=-{Hn!Xl~lG5bo+EF>B4h7m4M$mW~f$-d2~h3y0?RP-p&*! zN!R9+>CVsSM4|$!keaQ=%UXm6&XDdeB5X;J~PJ+rhTfgc+zr}4mwhPTM>&RJoLpiH>#xsZf zl>qxi6gOi0=v%W%)J}ER`E`T4Qoi((A>qT$Gj^v2vZGK_!4`P?d*8G=7s;XW5HTuk z0?wzRKTbyoiS2;Py{;wFX{0_2e7oL!=b)4SNhbAuBIm%jR6BDed(^_YId+LV%>~sB zEm1UU+%!}`tPCSeLDn?5$W#`#wdOF?>aelXyE5P31bvf4*b%a!j>K*@i!-J6F?%^v zVa~lD*!NU(fSpa?7RV+35hOA40^ze-MPeIOykn@DHTGL+zx79c1)Q+g_Ng8m`{7jD z??Z5rJpn?iJ`>QbZ;bb3kYMup?cmjMsuZUHj0;dXyLOXnYoR$^G5LT8}JWPdZ5=jI0R zwsB${F1`N)>o@vKEDanX7*nq#=sR^*wet&m7ncS<_gU&QwmlFEp*K<71z5k?Z12 zY>cc)(+e--`#aX7d`8)3qgPa@oUXS#!OH8E4n?(%iA$SQkFo?HYYECV+0TiUuVSa? zS8ZkzS9edkieB=^oL8svGmkto8XS1%oAI_uJuZ*|5(}{bp+Iu1Mfdf_me&eD3Tnk=ycR=8&^v&H$r0gG=d|bXV(;J zeg)z57EU_4>^Xz?1rBr}%f!2ZG#%GO6?bGj>iA;bD&fDMct*LlZNJoNzZ%65YDx_m zl>ph+9NP?p&hi(FQ&FCJu{Uh>Vq7k5Zu$+TIyzeU6eULuFlW?0m`su;(VqZgfz~r* zZviv@TY%4u|E~^6Q;onoRN}hbY$>iJ#$@OQjoLatiBg)kM6S3jS8Zt`U&Dx!o`gAi z+(?ac8STB4Xjcw>@Q9JF(nAKyBRafZzL|_soRK-t&)~8pp2AFPp{Xuc z=1>A!U-+osjOvr`RWx0W)(&@EgRJpb-YsABn}d}v7(Og9yq80O?Y*1`9Rk1%b1Vss ziy8Y`&WeD=30t|X@G@!Mj77F?8w}ZO0XlM6Il)95y|2oa35Py8#3`(iifrC*W7d-K z^c1x46DIHbQt#v;Tvi9_9#s@3XNu0JXFoe20heKV9<)C-u#z%RPat9di4x~TJy z`yGlHEq-3Q=3fUAD5T=}1l9 z)Y;~Jjbs)Wd=Vn;9zyL)pS}^~PYe~SrzI1pu zq98REz+_3=tB`t#m_A=nCkl4H9~~z&&UOp58wB5Q)gthq*G7xln$wjQ1}rsrbd7H^ z%V!4zqygfRvSpHX?!7sN=%R6VHz)G8tjEF? z>1twzw+GqAcAqCoU(Iy*KS%p6G4L&5>U;}qE17z&GQ@}2V zUs^w|;(@htO(ZI`1AbsCg3vCLV8l(s{Q>X=xA93cp!vqD38+z~@-)tM&CiIy55dw5 zFKmQ&V9P75b19?EAJE?4)>T33-ESoCQHMGfzh=S^Vsq*YHbnad^EwUO-Rnu4RYHeg zx(ypC1{pGcmIiolr!ST#%8u)ME5$9qyd2)K&+c#wxrTX=QtF~H~ zHN1C^W0GP#kNy=m-)0}BwS*_(EPbP7*=(z-!)n;qhI-hiG?P3c7QeP-rXb-Dx>}OH zr+w=f$A?WTBP8CAd~6l@BMu~@C*JNOze6~9sUh#jVnn!hoa*=iYRrq#y!uF|ACo`9zfDNcyf z$~D45QRWb79YJTJ-a(!Ph&w#A@Ny1ai)B~h&k0iERMhNWU#)Wo5hGf@wM&kZ3wqD% zd-(7;6lCJ}3qKWm^i_0mGB$bd7I^6I;6lP#c@ORxj!<`n1qgo%*vdh%j z7E#aMr|p^c;bTZp86pIlGwyzQxNg?A!TbD1T?R2zg&Jcx;kgCoXHesnyP^l}H2yq3 z6+C14br4c1?~}Lh2&?x7Nf0j5^mlz=Exq-^n?7{}Awj?UuZElBX!RZAq{n)konbgw z^k3Qrp;bKrUS{wQhEg@JQ)nRI$9WNtVvi^c_0D;c%p1e0H|j|$w!yVB1`QjS8@iILKi4np=+ z73`>C^M^7Q;i(Lar_2lbU%Z!%&BC)VNNpdEalO!*o}#qU$XfSWd2nc-flz|uXu&=6 z{4C1LNf(W}HoKe?J~)I1S&3fEd7tJG5wA5MKT+NSd%d6=yuF`glPICNK|i?g`1sv> zg6CiRDA}prdSYgmly@Ms=|ygV97_;_oB=V4gJGhV4x8_#ZmU_C-PXr`NTrvY!qYYB z#LUzT4hlCIgBw8kN9VH6we~b{JDcQ05-r z{gapJIVIo{_S%=k?1i_sR`{xZSI%5dx#a>*Lz5f>?*_=nZS<6RbS4$~a4P*dyte>a z_EAbDal1Y1-HTg5Xz>ysIJPe{*mq*DYl`lZAQg0dv|C<;6=>cph;UnN7zNk&CdWMM zzgEwfA$f8LZ}B9sE|L^5iR-SqtK2ft=1+O)XlZDCE zhucQXU4D?EK)i{=Lbc#b2x6R^XBI5)UPRdz84P(iFGN6}_tCey)1OVKz{+}I^QrUF z897wnm1sn6jw!Rp+xnm);VWZ-40DU~T{LE9edixp97U|3k!6qql*oZHYSo7B;VHAV zZ_#!;IjcVWw6TohP9)%a5UpY|Q>@!9DJpM6Ag^R5;*Uh&! zsxM?3T;cx)E(N)JlOHzyjHZ8y*|FN%{3&zTUWV5VCymnwdVv;5*e0DgTcoAI{q)~@ zb)sWWF?tQaeK#F99%h?d`@|0sq5hAdQA4%|F+b+t{ZzhIar97Y1TBp~&A9B^si!2k zRtZ$@8Qu+p)+>`+>R+ndCz3hCZZCp|hl8Qqu!_Tv=T$W~Mb<~HZP%X79gF=Vi+!f2 z7=sAeRibb>lV2w1p2Dk5#nxrEb?yNsOUwZ}c?HTNQ#rWPx9KcWxS)cXHeJmATb(Ge z<6pG=agO2eDifGOmx%~;V`#dd9@d$~2xi7iCSuv(Du$< zbt1T(Z{(Q?j&pnay!{s?&ir_gC=cL#PhW)Qi+PE!OG($2xYGmryz zR^2ouXbTMr*wHDE=+OEQjF_1epNfUp=X6;>?@l39SyvMKs07HdP=JS*b{0Hql_Mw2V5xuMIo3C@1{-D*JA8@ z>BV4a_N20p>n8K~nqS;U9;V{Zf^iQ_!>^^m{Y8b~nG4i}9RPE9qA&?*skSdPQ1_A& zs3GGC-VMNMs!Rljnu;_8{k+2JksfAF5plZSc!i!*gPn5$pWjQ7_mz}mAwo!P80xWsY`N`( z;j{Ul8jW|2Huk$1QP>rMqu^uuv5vi2bU1)>7GVMx(1$C0oL8Q?+q#iwTd|=SQuj_g z_8B?*@=3YV95G6EPUOZ9gbJ0WeTwqguXk8Xj{z6~P= zy0h~Mq?kqiw?|4<>?bs~>nz@&D-!Kn0HZSGN)QnPi*p4gmJB}LCz>b{Wq=7W??^N7 z*)}^YRhP&V{gT@EUG}4qXbhA7+b~)NEfRWNgtEyxnis1_EFLe zfuLWRSLHw8exs1Rq`t_GZ0B!+Ilgz0%BGb^ZEBNgDYksDkFM@>`wmBuVXXKTbyYpw zZuT=<-cIU!_uj3!M(L|>e15zXWO&ZdczLnM-WDrqL3v(_;}5YQ{YfwZQqyM*grnqD zTCOhPMeOiM2T;6IW*M8wz!2&!6;-sG0w$*(=}H+J^05ZPFT`K zM6k??Yd8MI5$(JT91}{tTd<6Y7+UnOGom@?bA!ZTGG&LZnC*x*V=Aez0}eGv=2{ z8~cJU1n1ZdjS$}3NzqN!RXA@qc&Z68>I7u585nW%AC#N1autW^ zZIU~fW-C8KP1_twkj8COLJBXMI$|vxjc;bDK851UytZOU_SKJ?ST)=|dJ@Jmk*k_63rY$_oQg2{UwkI2>ae?f0Y0tp#l-5KjrYCdroWTVtVuwhDK?B{+9EA*wy>U?RE^coi(Fv znM;<5*@z2LHc%>Z8*>^9`*cTe_&7^h7 z+R+s>CAlcPb?799QtGZH8UI{*48PZHs&jQ-!W|$ixgHH-vS&$DQ)dg33PsRWD3p0C zUw6%T$In*J#UM2XGUBfHylzf8M|uv_hzb0~>PNZNQyfF)dYjgt2RWjYiLa+lHnU6Z z#9s~xr-MDLxp;g1Y}16fGFFnAV{tarCOu-*_1y#1HCx(jzTrAr+v2~Uq50SYpnWEu zzrH=xiPCA3R>8`Lq#;puTp(UIu3nyaMzU-U`3CBA9}%)jC);Jpll6_7k&uhaM}0v& zS;FH~tSW`;d7tUDU_Y`Aq}N`jemK7%VR4L(&^D{CbUfUl<~c7Y_m}DDX=_d7c2@pD z6_SYIDb=_}J`y;Rp9wZ#Sx>h_zxuL)BM((st=BUbGx{&ZsWz>dPXwC-t4eH1CDI)8 zyetoeTZ~?grO z{_dFD)$Dl~T^~&<-lal2Wp?L`{#y2jqHfbCpUsl=Lx#WW%D0?gQ{CavlG(Y?adnCF zz1%QJc+W+S4p-COx4YSFE8l&%x^$t}(G=t!Yya7UAy_nrxLCsP9E{L4teRC6UVcNb zNhjSc{U90+u6HK=3kKYl_(e`;%Fk$B-dqmFK~P z#9oi0i?@Z7qW$!AfdJMjZ-U+#@t&UExHZ4u-fhG;e-o_1ywCnjULgLNd!_a&ak$V3 zkvEf)hZEKEuD0>Zgn;CHE}ADSFeRR7V&&s)f6Y}In-@>*n=>>qfK#9`F)4C{E)SwF?xeF^oRKQ zN~|0ZAOjcp{%}dK_UpuFjj#!Ac^`)tZ7dpq$Jt>~^y!YuiaPGRzYuI{KP@GfKq6+# zcp>1|n9ggAtj4Xqw9CQ^vuO|Z&?AW7bx}~t$D$@LzNF~nfwi2S9o6<3p#U}3$Xh_C zy(Pv+Ckwicxs8`Y5Xd_n9?@!0vfE!^(Uej5`V3dp_<9BZN(8!8<*;T3yt--A^Vwfy z$`ApyMwBrJifW$?`OiQP9(#hPdnRG(z+N}o zc>2ggCHt&FTd-WId2D8>sHG-B4CqhvJ-1U{j9_-FHXc*()XGk5` zR&Gnu>g!k$U`3Ppvwnt{(qi{{LK<*3GKZJ`Gt z!9o1vz0g6quzlSR5#W1W49b`7w}5uXl;hpryZ|?!ry3zD9}VRRc2Zc@k}fCb{S~_jt9Yfg3G7yMMCWh2nV~XQ69Z-H>2ZGh6fe`y3;_a2{9s3Y9gg6yh^y_nhpZAi zmPC1@YmC`}ApEuV$2MtI;r^gE*afSD2d4I^AHCoDGX;r0QerRJOFP+sn?;N>uzK9+ z$VI=SNOVBf8n_O!9M>f&9xL{756`mis-iV*C7Wc^WRAQPO;ln@RM)cCTI-&>Sxh%r*h~8~U4EyI)TF;( zdK*6}+L;4$+fOAEXH6u;kZMxahhkatXG+aln3*o9c&&JCtBMriqzS8p2&b0F)$44~ z2Fz;LK0YMA1q@4et_pMWe@@uWTvCL+Yqx)x&0V7|FvuOl65ErxuwE^ZsP6VMhd95T#JjT381_ya z>$9nRfh~>fSE(7B%ekFX99nl~$sX-&=yErS=lA8li+c{)$@z)WhzlU$YG%adfTk>u z+(V482#b13iVIE^J|K=N+pwUKnq>oVkK(w&traKzLDqS*W{5A>1}UB?N?Kl*7Mx^{ z@e<{|L|9Ht%g}~?fWYh;=sGns zGW0(!92eFo*T^uE&9o5gD8;tyoA(>H!0g^e>C}7l>w(1AG|UKbRFEs_r$|iUz~q%Bf?FWrsB8KB0UAJ$m6>#)b32jLUg1a{59rbTIg`cNdMIv# z?mOo9ZA6=<{7MW*)F<+(4jvDrG6mt~T1O3^kck{PoD1|cxhpUm%R$!mPMV_gc zh4Th{T!Yu~dC@Ru1wo9xD%9*B?X#>Oy7qV!Bsz>sp&94ik5OY}(sLoYcHB&a0Fs!d-V(1^o06NNxfC<(oF} z0q6edJ$)5KI&9~3x=E)gaZ<*MlG$Gic#1gW$6)we;XhHlXINQ{o+Y-sUJdPf=qkhE z@pr=lG2LsT4DqGY7XouHYmL-d_^TGZ7s`lp(F(v26nI}13L=fM*PP606y9CM`lz@O zto2h}4R39^ioDO?7~WH#w|0+5utdwd&iX`RN=*M#O(Mo2b`RCrkk8_B#MvN%kRok5 z&kb*-PiUe!yEE-)4sn@B%RQ0RqM$pkmN;3i--a1G9_O1u-|P+G?4h~BJqZkoV2Dw! zU&;K!?u>@jqi_(*qcEc^AM>_wwX+jQ7#cRd94fLC@w7t08&dN|W^Bn>sL!kI16a@6 zD+2BEz6Bf$uBu z{AD>lL=50I3;@MKJNT#2M-2zQ+i5SB{d$x}9D^-80Ms&$!IWY-yLi=K>45 zVkkPK48=PhB-nX+_R>&^g*eU`kDr z@jh#<9AU0)p-r(!v{&_4tttrrrEff$f(HHV!@TPx`@O;k!+f!P(p0*0Mo0sATSc87 zY|qd9;~l1(ln0@hfNNHA&zLlg@8^g!vj=-<`qwiv=ctn}gM_@~co!TBc(Ap{A({^u zz8_d6M1DPfsUL^J}7`!C_|1pqB=P z0&fQdYu0`yEHf;b+nooO)`zd&1L?{wIq;HdWiJ88Ad&=wNe!H7!_sU9Z$pA?0z{$_ z9swSypO*yuhav`+$5jYdat*?3Z9>?CI6_48yBHfDizhRjk$AfG&Y))dgcbjcz|ndJsA{{i(4BT2oF{ zdi=@+>EZE@C0!abG_d1>!RvwO^ux^uMl_@A@e?_~xCNN9Qqh$Z%h#%9Tff-TrdOXu z`B;Y8{1R89A-8-p8X|^`E|Nvnmc6L9Glbmi+sr5PI+{us6-t|f_0$TmzULx`)%3qT*wu74w>px!LvW_jI*c5e9rcs+nGxDP~OV5e=c z{qRBlEnpQtNo>&^8>9NI<4pGo^^jRVF zb(<-0W2j?5hSxb=or7=iTOl5cZ#+nfdCUBt1kLfvbzS~;x|p$=Hz^dLqMyJr38873 zk%3TezKt(Fy^E&o+EpG1jGT83ldUqGuIq- zprzb%cOl|EJ3Bz@kwff0(=Ik^(|mTGN*}{I3Q5fs;OW%p@s@CqJO<%!OJKq|XZrL- z>UUnBYa1~&>V1+rV+m2fq!dN&ZG84hkx!Jj_GC@{Z9UO9(XJ<~2wY%Hb-K5gE*81Z zZ}elV+O+XUyh|st7<2ONCB_3yw#KQWjh6Q}FOQ8|lo+nd)fAwrlO986`Y%*fblclr4RG)}(o5(6 z4sE6BZ(um67b-{Bcmrro`lV6@iYl=#^mm!jI(`woIOO3(P~CY#_$_FpQA?_kZto}h z-QC|ae0dK$z1;4hbFCBKym70Y5nzokJq&Ek6{`p~JZL>pJXT?*@S*$o*4!1%MTXE6 z8_oEnBL&+HZ7kPHxhi!8)rog~6zyy#PF&!lUEgl2a==5cR{t22--9Hi9- zk4TdQJ9UTHZ=|-$Rv6FT#0FSsq|8trTQ+&V^mSk-L$s<^eY51wMMKYnJ7o7+7ebyE z70(gz9Z1v1{AIX!7=a!i*89KLPMKW`Ia3Fg74HZLHapx*eX49RI4HCkBx+<;Tul!s zUbg!TvYC9s%e&QSCcLVb@Ku~JQH=w0KJX4ATGbW|Ela`bOHX%RoP_ONqU}{@%sYSc zDITN%?0i|~t0wW>`Md&kb_+gWgw}ip7_%l)65&+nZM1L7)jTzuy5AjHgF)uvB(0|A z)5+pl#f>Og{ncu)dXX}$dKK`+=i$LD%yk5jbcrEN*3N*C2T|{x*gE-6N7oPWE0=U^ z66Rr9TDtnpRM?u`L;B?}KWBt@5r{63mLG$D5KG4--`Hx2+!_+Y`tiFXR-ytk8ldaX~f~>FFG8JHn`<-_U z@4No&W67@YtqKoh-(GrvaK92xvIpPPG6@KE^@f1N8hPToq4Iov&lHq_4HSpnPC4?$ zRME%xvScBPxI4!{(HRy>s9!7b6`GeGE9K|hiRAca`{x+^QwRR31AowgyKNQ^qiYJW z!qy!33D=yI`>q}Okmv%-J~?|k&@qPD)fGBmVvirh{^#Qi4vn`!RyJb0m?H-dHScl@ zNP^&F3iFPjts;~-CqBm=aQy;!L>=j_c|IR^xpND|MutQJbu(_E&lyVBVUnr>2Mr7- z6Sn{zIn7P~EwG3xOqG6cQ@@7t1+RUH`{TtrWExnvz@Pfc`|~At$uK~F?2F^i&mxAo+1K}$wi;td6J z@b2U6KPh7I#Gw-E=#1Bn$YX>gIg;?@yEe>+h-_d+2G7rs{Z-I0d>zJr^U_ z704Js>HpE$`#)^L?4C8v>1ZJz0# z(;U9ShZ+3x&+bP-MN1-$Cr74%@prU4{}#M7craW7RW||PeMsnEtdS=MH!)myOinyL zx?sKqyspx${_!=;a8z!`5<~yQNa>2)HwZ7GH`bkSQYD3iaaHpD*alcKo~ z4I`8+Df(K+RgQ(9azkE$PB#n}$?@Oo#p&!A+zQU*&;Iko`t$Y%N;pV=1r!41LHDOm z?ii52)(Cy~IO^w20eTB%f3)5PcaXX7xjPlj&$~~@y&2ho}MANpPcha-d9XEZn&k5{oC}p*OygW z)aZRH&AiT$Ds2<7DCb8JoZgJFT$L$CV)7@RV!IY=6gFj9d~#m;OSmCM11w?NT{IaH z-97|YQ(%OQJ}QJAg}epMb>DaDt4$*{YkD>9E=1?J)Q!inV|-}uIElEwxp0_|RqWH= zF-RU16;9+k%;v!`c~|k3b;8riJk3!- z6Z^hEx~7SDJQb&fS>C+nu}_(qrhcWo#+I_2pQ*G{mI#SFlVo@H_{^AvgB;MBeQfj2_m8R#LQ3bpWf^JP)B=$EZ{#UiXaYiU@=CPP?QnVd585O>55 zIPskruX~nhzO0qC2-Tf9i23>Dxr`MSAuq;O7NSb08Fn_ET29B`_brWXIEo-5%f2i^ z_2=MNwog3&g8={|PWdM~QXJMT#R_i-uN%)B(AayA!L;T}@lf<^R9s3r7h>qq(M5z8^B3zSJo3_kV{VYt); z+_$iNHrA18^zR%4U&3nEyees27&}W!YYYUNwWY`XU2XawfE$W)|ii zM`#q#qLC)XruK3iRt%n{@2F5ol>b`rcyDxD0+c74Z~N5fdt|hRU{$P1^+5Ru&CXLZ z8wvr%NM0W8pRvYn!IObZW%K@u<5o>JEJ|zA&WXwW;7(W0t&i&gPTwC$eP%mLzzPZ; z%vlfK26vHXmF#*p;K>y4dQXvQ8qwSQqrT^|V`cyaN4fv-U*2241-x$o@qhJ+wk5N+GedO8ZU%9@>)vr&x=r#W~PM~)RD0a>fByaJAR zerz3=bkw5@sBule%hR_2b%C5#M#{&;HHtF%>CdZ1djwM{wwsL+F;z7E>hyV%d!=A% zTYH?}DxOJ!rVhR#uk58&iestuuZg#$Spu~j{Wb%7sNM|i2cuA+JlT~X)RP8Ox3GU#JX zftzTAJ4u&R=*c%g$sodFy`u)3iK#d>!`TzpfvK`?^t)N}2fc7$dKaIlrU1BOH>N$t znB1Xf9M6ZrMkQqJJ$N#-sSYZ+XJ35fqkIzv{j?_8@jWHaBWJFyJd4gm)$!3MMaL%w z82uZbrKek2O6B4ln2<@KY-~gb{7hAA#Gr1H{aGb*wM`loL(3gxrSDo=UcUX6BRUm| zM=k5*K`S1bK5tklTSJOoU()wxlK6X5BnM{kBvQd@mF_OZt4O!XxuAHn7o~FD;Kg(n z+Ss4`0rsEK+x8r>!PUcrn)V@P_ie9|cBjYkT`OvflDujUKlA)Z>J%NaRH(9@>xFI>dEYC8M<5D6)=7uv&oy`Nnl7*DF@~kSVKg`sr%$RAud~9_X=ea@}DkG^|gvP zchzY=UBUH)Ou&loyPR^oJqf1j8#C0sTzvI^eyUoOrU=TbGTv-5ic z3^HH{oeiS7oND>oW20C)eRW~VL%e*wvfEZn{HZy}7e5Laj8|GYG zC)@%pH>&(uw}30?0$lQs11zli=?RmCJ|`!tL<1VT;$2nG1f)=huyw#$vzoJiD=CgC zL7-G}ChP`k_V77t?`F(2lbC zgSn*4A!mgb!vp#UWd^-`TlwrinMbofU(FEfWljzICTt`rqIXHM{4*^2>sk1pd?!|K zVn?H1?Z-P8EWe}P$bX&T|K%Ng5bbE(UPKszfH;Eeq}i?A%(LNR$i%MqtIQRpugPW=}%id{YQTxr)ETU)axDF@PiMuf=A!u z;8Nd%^XF^$bqyAn9>28+0tVPx0x&xt7&|mh4a!nz-pS5vFKRfs(wDU@unD|Zd#&3Y zY27QagKZM`g=jf^)jnYc>Ecsz*if@*Z|5xvdKTw4zYQ{sYN#G9`JPp~Ts>*m{&>7q z#Oa()oMSyzltk`3dWDV66{;PwWms4pP;TrqQ~R0uFbAr?%^5>Bsl}gZpg!sk+FL^# zO|bZ3ApKsJaYf$nGS|imd-Y*QoY}~9qYZeb4+TTH%L=BP^a;MyQvAjLbzec*!2~~J zfz|B`yfFMGp)B={vEjnT1RZ%x)!7yGX4&*_GHYhI>kbC}=ul~)YuwJwJ)alV5)UM_ zN|Z<U$o@hRqRu{}uo+OCjiqZA%8o=l^EKi-uQ#+fm6ho2ot%Ugzsoq$ zC~;+QatsKKDy>L*@U$*7vAyOuH^6vv12Q~vmweWha#6*WdT!x-KW=fPHT0-g9pZEl zC8@AwOnzceVwmx+b$D0oow1V$ShkanTExFRlJF)Yjv%W&1VLLOo&d zp)l=Q0~KG_)B>eRjF4H1<56^UqZdtd-hltR^2$`^AVgCF1l_p>!`lty|NfD-Q%K zGKpnSb9GZ&*Xl_(nQv!|{M(k^;?np=d+ZJQrWCmOMT*4wRtb95JpF`wS=^AS*k@yoO^}G~vrnwN##IONLPpXHqAfXP$ElTzJ4OSv zI#Ytn&-znj2%f_RqJ#RC zR${}2N}xxVtn^ROTf|NKOb5Od-#-z(aeE0{MkYO>zRcb|v8n|vpQx*U4-(z`Ti5fy z%^i`O_YRvpzoJpZwoLwB)zF5~H0S?ptYL_)iXWC5ONh?pkRTh8s)p@|k1kFXKjQPX zUdeGpXZZgVTaXzRKdm6Xza$5B!dI}e@%*u~-nECC0B5c1rx2grL)$I+TwVu*DKT-S z=g-b0K|N_Krp)7OQ-a4VjXDdV;Ehs`zDZAH*`Tk!)e4yOCS=|Avs08o`o%kSk|tF6 zB}(W?Y^%_@LAqX@`hp@jFejiF)N=|N*?T@Vo21E^aGr3PAxEaE=k^a7u>U!irfz|6 zygV<%?4ZP|^Y#=gIPHpmZk&T>95-Hp(2ktM8^FI@Q<>P9@=)=kltKSPi@wHPo!I*Jv+d${=0gLQ1W}0oO221fU*p@eh^FDazI|5lGD_|RSmdh zSF-uW&i4AsO|s%XRcu75Ra;CYiZ z(K;-SE-8F=3veEO-=@c{8R$$6H*4eWSOGoK+OV+Ebk+AoZ_om}lVg-=70jIASkd_h zY9MntRbk(IZfI;~KF%C#d*6XDk3`|L&#WtQL+FTNdC%&L_PL+zEx@gDb9500-Y`Dn zHQN2*p`*_OYOM_bdz2VMde)D)i(2TjIz7n^BRPjX4}V7>{~Bb?7taloO8}OHL+=$_LgfeU2E^P_CD*J{m#4RzIXrNSJkt+i>jKl<{aZ2-}r`nd@k6# zzF}BRR?0p(d@UdyEft}xaV#%l#iSD7X?xf*-goL_B$Ax1{+Zi~1X$Z}{>n4`{da!n zF^T-ZuYOC-`+@6j3jEmn6NL9CRF(DHVybirxEKse#bH*5n&Fz#@E2KUH`YCKav;8^ zZF3e{Q>nlM(hifFZ@6$<%*AKTRCsCuCvDvHAYan1y;mIrd&w#?g(J}&>ydRw&>*Ro!)0kp^8J!_DcjX8GBO=p z$;4FJS<@kcl}Q8%f9Jd-1yf}58v&L(tvALuI9?*tqlmN8O6%>K4!9I=fByQyw!MAx zk%Z<*nH9&vb3Ljjddw~7-3{ihsm4>Qd?3Q-OvL`6ZEFw16pKN;9-F4ZS&JtaEZw|N z(C4>-#q=~HsW-GoDIY==t(-fDjHQodHOjS8LcHG)0~E^0Ho=6pDkME-QbG%md`n3R zl^J11j|t~r4%&+?m%dfai|yt{z`phD__h+ZZHh}@^Q1z6NWwP|PI#yF&LDXQxOB#2 zv6A*};ZDcer6Pw5OX)(m)(E!GQmnk*_>fsbx4!D#8iroNp25PGiTDlph%iM$MTa~~ zqLrX!N%9;S89YW$se8e&GVM_z<;$~yF|46!52Mo0W<84hV$5RXo)g<)gB(cmt^Gk^ zb7&EOJnR7x$Z`a1D0|45B-+vXPZ}LCi2lK7qlo(Gfnl3Fo}cFFZk93LL-e)6((gp5 z=kBYiPtR9~K3R*rE$U^W)?Z$EE`3DTig`V z3@wwlB6$UmwkSk(XLY1`arAM?BwkqKw(VpH+s2=*3TZF2bCKqn71r^_I3-!LTBSLR zpWBIP7$`}A9I9F0&g(gKX;ym+N!bv`z8vF_;cg+9S#=7#g@NBp#-d21-u2Au63>@L?u@^E{$f{Dci3}i(&nCum}9&zV& zsNE1WxCTXb&zX-)peIrpx@;4Te7(( z8DZ>R{zQVfTJi;MIA}k5PeflHs7y57^<`&gobaMAh zOj0Ee=Gt?r#?0GVDaPY3zw9x${|3VQnm9RYJ9bL=jPw-q2*%xCS~M#THx*PNU5+-D zuN$B)+yuZ0y89m`H2JiN!ryVWvA2r!HOCOgF2qt; zww#jrpyc9O)AHVko?oADZ$-4Z5qtx6+F*NaHO}%@hWm=GfN8qM6q!M6ui0wDfUK0AY z>3_Zv`-fMaZcp5fmO5{krOcR&{@hUc4RqT74RjS`avRlp0p#D;`yiD6Z}Y!?CT@hy zb>w)g1suoSug!)Ku}Ck+){S!Tq+1}dQp#RL`I@uX^wBnl<)Dx{1lz}hn=-*N!d0~+ z5cAz_!sv!RdsVvr*+7-r+4cpLcJ4ROt1Te=*=pfpTI2&k1{|Eubo*DT$&Y@Hzoswi z0X_AB6N>MsMFg7A(K(6h$IV}O3EL=m(kZosCldiK;YzM2m>qy*jtjWD4W=RI>kw~w zOQ%=)$yF7HH?^kh3T?llvbgOwhF?2#dB`WqByO6r>P*U&vbsaXcbSOmtm%s2u#cUU zxb)|Zl!hVBlym)KQFGemaqcW>-K=3Pd}+Tw**GB%GM8knW63b8pO++FAk+6y;+jWsqC#5M6@JUz*}0W^ISowO=tC8V_mHc*0xC1 zDf_m}JS&^W)%`X?0qs=gzaaoI}5XJPG&nHDa7h<+c8N_7?r$G0d2< z`aOv3v%TXtXv@lk-nH1+ZnGm3M#G&L+3lR9cukBQGpFqz?J5vPlGGMxSRkn(d82K{-fIJzEu>T{T`*8jP7 z9aw0a5k3Wjf49(DxVUS1-qQ46f6;pSKp7?N281FPv?u=fAMknN1~x?a2ZmG!Q$}_k z?Onap3JIp^g%C76w(Q;-6vE7dIa(lvP^A_+_tpf%tI<^L=b8+Qo}>=Kh6^_uLh1VO ztkF~oan)*4^7va1XtQaq{>ikX-TS{|l>ASb^=?55!Y`Ik8xdS>Yyyg*Kz#20)=~P8 zh4(*Wb^Q*&efVR}VA%hRLhR?oH6!ZKpD>Dkgh+pE0V+iU_`=p^LoItZ_H+%%bz`}p z$Dq#xaxkYioVM&QQmG%5Zy!?6ZN^DDr zQ6uNe5#-<74V#_em7WWsOs~GPFF)?X;D{#+t$K$IoOv`KDpNOLTK%H%0IL=9j^#t} zh4(9Ks4yVtoD-0l_9FN77))KIRJ~5|Ud%7TUyp%AWQJ`Efg>x)Zy?cNXV>%3ecET=!exbe&L$SW@@(K=3vua0{=1i?UOrq2Fl}UZs0W9u<+nd z2p0X6acC42OM55(JPr}{m z>Eip^croo>M%_73tjX^tHKclLgguy<(v|D+Ab(RQXvRA}{-Dovh4s8KuEPI6Ljv)5sepU9AiMZ8Ka-GXhXK?3?%Dql#G{_Oa(H>%BBM&0$BIn z2yF`AQVy30*CIZ5Kuk-TI;&lSA=GZ-QJsplnvGe(@!9l|y(=dE1_bS&2V(}DnnInG z>b^jTU3-izx)#Jt;>fJbcTWk}sQpxJywv@AK-T7C@i@cW?p6#Q>@MY}9JzRIW7}pb zFpP4HlB|vQ&-#2F61Lf{Z;~0rt_EmNK0>t36Jl@qZUBWf|UX0S|A=Tf1 z17W8g5Hvz?_K?B6R_KbX_O839Q56J(kAm=OiaLznB4R+Z`+zvf%=jj9wJ@wBeM6as z>qdREP_R?7^)}~luti%^`ugP92SEFo`0LR`>DeGtm!fdmV`!5=i5-C?P7dXrE2j$u z0Y``{Tc6Ik`^835G|&3x`7@91y+RYa*Y;ym*Z|-GI|PWo zs8s{Aa8<)r3Phmd&l*Ag|3Dce=xRc9LFI9+AnE3ut@v@>wu;p`K~6u22tb) z_|s!>|3)eVR2xxmG|}}9Cpt}kmsx||Fb%z!+cf|5GvNFiBKViZxN@o`V;|;ai4|-O~@_S!ZWDf@%ubW`)3RykG*zppxgHl z$0*AxczhbA-vZ*OOOB>HUga%{{q6g+Bfv;;eBwAfTe9*r=Bmp#kP=}3FPne*mB={q zW$8P*Il6yR#q&NZugN6ICh{c=&V~pcnRUq#_i1sHbBmAjW4*z9-{8z5&gsGvd_!MX z5Be4*dy!zrHC54?+jCw{=R5rkBo6RF$f8*@XFrX7Wr>E}c>i9)3D9evn=rM~835bg zzd*h{CHV%r1NivhJloqt)tq1MQhWvw%J!>&`5`60q5PF9srr+l|9f=yukJGq)RVrf z;@sk7lWV;KfiGQv1?HLa9yHW_9K1Zk+ud`!GNs?;o5!TBhPqT}sFEyGc#^o?m{u2HNFU84d_E{7mWmisv=Twz)_o`9au;}E z%?Zz_E1|btShvJXt14z}pDQA}JqFHs0X&Owl_qpC^hMAFg@XF0`S+XnZU){cPm0O> zgnS}`W{c@ZX9g1&B6Un@3f!8^N1i>mR0iTrB;0uYf$A}*Na`1!Yh_eD;O2v)0s#}o zR;}xe^Y4c>h!{Evy8R^bT9W-4v3_F-JjF1*ni%E+a!$m`sWuIF&zU|WR^kdm$q%Tx zg-=!k5qZr!vDVy=NB14CL+j@Zc7YyFa}bE~J%ZrrdVXvonSC)Yen&zi*At=>#p*lD z<**v>YZ|^F%+_4!S)&@sZz>i}`$f`coHAqXpw)N_4Gyy!MT)XZ^#iv7Jx{3^D;TV@ z($#Gk%@>;L2mpC$jeSr|MAumgcvI#+(CLaKyWb1wtk-Y~ewPD~=kaF@{Ko`H?B-vM zVb$Mw@}OjY`b(`r%afi*ysvp4_`PRqsOTTdwB!kKX=o~%(E*v_tu}9xBf^oMq6KvM zeT`&m<9of)f&j|-`4Gk7<^}G)`hC0yMw~q;;cLR7z41=qy@cbe5HP|^j_*S*8O^ST zjjtCwqmXFF_92D!PFq_iLy6m}w10J&e*fNoYruh)Q^tm)2d`KLq1 zBf`mpJCcIr2z&)jn^5iT-J_W2zrTxXWank|L5$OJ#~_f0*%uZE$`rGzbG27nZ| zwI*84ujHcT#9tXnhe?X9=XOe?@J(Th_!|6L$6@E0kx%koSuYZKT6sj-U*5|$97dbia-^GwzJagpl34@{lEaQ5At-T>h%HDeAK~Mz~ zL0g>$19PqILUO@}&#xQpjJ~6agGuN4YjO~GO3iAaF#jS?6Or!-W2aT*!>^R}OOrVx zU;0FWN#grN@mKyZep0Q3MV1qQSx{N-@9czsoRt3jGo!Aqxee=L`CDgMrVnD9GOZxz zhHFO#9Ij=4Q7ce(F@VBT3&DUbTH?XT#!g_qFSMq3MkoA*9B4h45DEQP_qe-0*HX-< zmX^PgQ*HMR#&Rca`#9%7vxJs|dfv6+X0Wu9h_Y$vYkg9l%` zL}M35VqQqCdzGzv-P<1l&f@fp6+wdDb);f~ygkXvl7P(yXFkBQC}+=1_HTKUoN!x3 zk+Jo*6>Z?_>=fDrL%rFz`>41IfsV#amQR2NjTDlwH{ATrRlC??em(MLX$OJQ_iIrlJ_=CF& z`n??2+&-qcW*Ytb8S0H2vns-9Drv3%vimN3hQJVAj4 zmIY$yvt?*-vMDv?1w`YuOUZpOXf#6dvy zza(Q?Yzd58%Nas9RjeSg{62+n?acq6D@XfP{z`eAQ`apb!(ArhCY^-=#}DS@#3e@j zKFa7bS)IuWXLRew!#u=5xZdThKv=Zx-sK)gIXV6HP@Z*M32k=5cK?4pZ_}< zYB96Dn|sa^J1em^vj_d~~;RAvMsn&upw8~vJcCX=|&F9plc`TISUDtY&Henr0W zeAWipe=yZYxTz6kC(2?Pp$4axls4Q_f*5Z?jtBvVut6S8G-}{7iBmuuOl}~+#dj0; z5&2Q}H_(0@+XZId1scd}u_$1L#R`dF$P`$h_Yl~0!lftqHy25%jW{r~u%K%>&N<@Tg%srGvRdUVg(>sIH>IDho*T|*gCBindvIdx&@X3c8CiPRWb%UIn+`MQzR`WGtm^KU$*UW;;1 zZ`{(|q`JnU>;b;0S-x9?^t`>e={f(4(~N9U2IihXW5?_YJ-}+ja2?<*+tar1McQ z;$v!=*D5aQX=)4-X=#zNI-D9|G^el7%~9onUb8LKvd?k#;;yHurkTU%>m zmVS8qXu_k5&zh9lYN`@D-F+$DGsj)L1da~RY)v26v49iuu9msXwZGbTW;g?CLB+Ex zyTpn?l@Z~Y873Sz{Rv9$&l|P}ctz7;1Sj|M>$~A%DVE~koL^iw= zQcu*yKRw9hb=rs4(FYaJ6ReReM)nS_HP2lW5}o{XQ&%>p&&I~`cBlHdWa~_MylubP z=o`L(|8rS=L|F?v4**+;T#kxKaD}FrpkV>g&lT$=Wj}Al!c|ACr?Z7~n7`A5xBJ=rw*>BliL zs(OH-k;C5<4<6ZvRpEr__V|T zI6%U896(YNt&^eGV4&Ed85#Qi{b&s#mP^#Rz_R(+cOKxfkxNkD5qPam3qJR;zK6+T z)bBi+aBrBf8iuI%IlHhR4(^^!R;SsaIplMrPtINYtG@rg@Aos+Bg~V}PK!4W**dUy z7y6uO!V#FrF+8lB&!(lY81=PDsOfiQ8Vd}DR|CF!;b`46T>Eg4LW4-P&O`OhGo=s` zc{vxe;^~%n!*|Orau%^a^7PcD8_7Aaprj6$_Vab#+1a@bX!RUC;w-&{P;vyXRS|7f&jsYx3Os-3vK*MNV&-yo#QgR%Lzc`+HV77bBo~FZFdv8(hh)#i(W11OJwhu?4@) zC>1T2^JWtxdrIB|ody<&YFY!1IBYqT~;dsxEn+I);+LKw`uKDd2m zCG*Y1!0W7e9}Gp{grG~<-upd^Ka9#f8N#$fv&?GfovaH{#! zR@s&%L#o)SkyO_85zCIkad|x}gW~45I@rOCiECRs$9d)>4i{GgW5zdY)#r7CXgpWr z$=oS(>-5J1%?NjssYDv8*W~q#p&g>nG(1weVBhy{a zZ=eL7C~et=l-ih^jfXpzG1fIN;wO%Fo|>ycs-&JK5$(zDK3XjA_qb)UD&5>`csjKj zsrX!waA>qL57MSN*|vWF;bb>_CF|@9q!f~&m}h7p?<~As;BipayA6(wYec$xGS(TT zSaoM;mvvEG2aH+C`#+*6f3n5=i8nC(W{&;LQqjynuxUVb251Kh9vwW6y;=cpaiNM* z?5u4nc!~T{Es}-5{p=GMs7~mrsQmQl`rxOt2VU=wdmhT)(TM3eP**TE27-1}htnF4 zZczaa?X_YHh+`&Zp(46iQVKmnT2FWMmD=)8dj0A?w58(!nvnA7Pwd1$b8RpVe~m_c z`k5>CYn5$(5Ti7BX%T!1JIvhtv6qOS{bMTmvHdMMAnj*x>2HbMDrG&*PpHk8*@Jzb zEgfWLCYqNO0x;k&RLmkvh^z7HMewdWg#Vh|8OQBepX_o%n(+briH?Q%>w28J(-{z|uMWGtAtbGiF$NB8ErDaN~-b?@K)kynCbXsbg@0d)BmP zd{gh@t_ShKykF`of((#pPKdgH;l20ofGV&aw|XoE6m6JV8SmzX7HOz%;%unQZt_lk zX|g<@3aAoobX_qgA(RVO7Q-6jOeKa36<0OE$RbTe8XdE29JY2i4Dl}_T2oVB(89T1 zN#)}t;s7DNrBybd_p+l5;HSJ74mN8VJAZ5wy#aK|&vE*%_G5lS#q(yQ(4tI$5QV`x zi{alTvt!QfN&$i@T%GPewSfMWBk+e~ZqWB!zB-NXCRwO2VT53RBIp!ZM=65eWJXp~+}31tByHZD z{8U|@CA?irO3v6Yqu+dxt){H;d}e0+`!M{G+MlipLw@|8=R*TkTG&{)l%^DuGMsEQK5<9_@Kr5V4M`Ty>1Hi@?{yP52{=W}GFuXreZ z!`bR=^zPLJ{q(R?P5Oud1m|P)I=1;if#iY;cF?h zK*QKU{-wWZA()Tx-kh24vK5Sq)L`9q510Q{E1|beQ^Q)WnrnoQJ}F|*MXF^larR7= z*b*1IcPGF3V&THmGB!CmPDVBClQZjURVXu55uWhzmPQ$ReCSf`0w$k=$1)7d?TeRD zZBva6CZgnAM161%AX-Y;pJxG)j%?%cZ=hnx(*&6Q?E}(dcO0Pj42<_i=6S|>^VAq> zskn@9)Tcb7LCtP3HjT{$9L~o`pIk}Mn4GsUX|kGQ$FiMhZcr~ke@s{A#tNOE5ft~? znanMcXT)z!@J&G>FBDf~I%uR-KAnuOa1mZHup~E2;3+qY+ZHjS+JxT1)v#6+=Np0=~T5obm~RRrK+hxi=io@Ppfa{ zW@OT2rjLC2`X@#M#-Frd0*o#Lx^vjs5W~;4`RDws76O5_K=z<(%ELeQM9J0uA)l7W zS^oU+4{{l-8%W`kY+9bzenPd@XH84|Fx4GR$ifXEVSB9`G6(o&WIj@3`3_=_DuU?_ zopui~_}bi_Lv?@yANEuLbIV=ve5C=AJ$n_(Ry))6EOJy ze7zS-S8C|npsWdgxwl!8)+%PNjohn^6nyEij&XLk`khpN46u~pe3wrUF-D^^5Tj+M zP*(ZvAUwsb!{o@EE-UBZc(W7Nyr=vP%KfCQ_Fp~doum`+(w1Yi!JY#U91P#Pf!<$3p7bvLI3(Zrj zs%yhKS4iIF&|P^{NlL2PE0h0T3*l~Tv*bxAHN=`vSJYsCJDgu-6W-qOhy?Wd4vl~K zLS$a*SHsN7EncN&iq!qh$Yf2tdaaU(XUDWw^Y$7Z4;FQa_Tw`knX^qXZ84{uS~-=3 zG|wImmEH>fi1o#7*tapD*$uxeXtp9-Z;my@=0(}iLOIUR^kuM15xFQPDc|ylm2tC% ze8$FPg^y`fWl>!v4FNtyLn5BCF1utc@m=E2;G7`Em@c$-jl9`w?P@tq8^zSi~D{R>^=U9Ss-M* zrHf4PEvwT&>xn;xqvNbH6r9uQwzsa8pKW?S4YM7h;m>6wa0#{&)P!57u~uPCzAZAm zyzsoaq z_6`Yb%85jRy+(SVswD-XR3lk==By`t4~v{MC15MCd9|va*Pm^!*+QGO1x~egWt|B zi%E2VfFDUp{!*{NUAGZMYzyL>>yvvQ%g8IYmquuU604#FX<~S$lYG|+AG;f6)L63N zxzH#LqzrEt9JA=S1kM>n^#qd`Klo^nWCTmx5xr~DfbwYlF0P}pg%$y(72u9GXa3l> zehFBQ{t}bnmh$g%@YQQFfns=WhtUs^W@K8p*TLTEIx5g?4@A8)@3Q#(wh%Ty9k2Q({tT=UZ) ztQnbo?(AnPM&m~mKJ$;5DiD6T3r2(Ytt-PkAWJzH5XQsJ&-v*isS%0gV&Ai?e=$FQ zpd_6xzmg{YsHtRufau0P9v)ob7~t*Vg3KDgDRS!Jl757K4!*}MLOQ?L5?GoqD< zaiGRwBLii}~OEn-{W7TF2x?EOI z$>e?h2iX^X`+h8Z(3ay~pS3TNXnZyH>s9Z_#weDC|wo>;kCc-j1;D zwvvcd9eZQ!f5 zds<_A#~l?jUFU^%p>#M6KF8cKGDb4A;T;dd3YDd+M(l=O0E0z2+Ki5%8G2Iaob85t)lhGjhpkraW%abY3u^>M$vWX_`h(B`c1kAd%3qEj{HdvVh2p*_Wwlslhk>k8N%u9u7VG zu<6DLoRoJ#`e5}GyXbTSwbhl+l3rTr?(Q4UptKO$hQl57R3-iRp|UMCkm}0u_)iSC z|HxD#i5L`cc0!8+M}EqAT|;W71MAm zxH`LDt~7U%-id57V`mu&oPA-~*xTHAZz)~^WDJPT!@3Os8&#dPVQJR}xjxsMH}%K1 zH^2b+ORK_vJ2mgvhETf|$di|GcjD>nSd-sEcS01ixLXmNlIT%DP3PGZ;Ine}CoCAW zpx=y+waA-!3~W5A==>b^$_=ngQB^33CuL3o4DO+hRPBppbl2HCA;7%yNZ{|xgvP~{MjN=-5H6QIUenFT;)dkNN zY(Z0niw*nJvskj~+!0Bb5m_u|$u}*j3SPu!V17la<;1;NelX#()GQYU3MaTrP$VsI zW9Nc{k*a#vq4iwrpl)EfR6X{b^k{0sidFSh(HAQ5b*ZB45i?GW9{}$^+x|Pie1uxb zWhEIp5Wx|~IG|tvDdjHlWL(7)dekR=|GXiJZl8#)#nW=9y4t*m$FTGqvHn6Bav!#) zXO%5ApMV(^!Zu07TZlgD>G!%zm{>p+sTpGyDak@pWNJJdK0KzbI5VWQdCC=4Lw>D5 z7U7=ElJ+U4r~m3;)(zq%3xYhv(gEdhtBbv)r?d9Q{ufU~Tt5Tcm4*bxwweOUErxV+ z<@J-#QqiF24PUP>Q3bOfT`6r66B9U`u-bfW^kR90WcT!lFX8r4()htI^Z`}-@4{a9@*as<7VWZ4@rs|wTx(09%Eb!aRkwRg3>95pBX zLi>&L#KUysv^|V4dI4441D@mP7!%`Ru`zeeNwXN8(K{V>Eo{ z=*tN<4k#Pgw>Jn7?*W!=LXMu`#fDC1v_94Guvjwu@-b z2IOMxy8?5vvfhA9n5)Gc$9|`q!0DBmMpud=jp?DMaIXT{Od`FC0=uhsNZWcWC?z7=|k z&9FVUG_)b{l;>Ih2v*X_D|_G?pNsymw0>-VvNV1w44M4Aksmi@>0+;KbxX^Cjkaly zwo@<(ol>x%mjUH6X&6cwWf67umON;HkuFm;pWV{81k4#0bDLK$26lbT{A2=o;gW6z z3kmJ#YO?YQ{wy*AHd(8~8ZbWxjs9almPQeA)PB)!M#Zp0u3K5wd-?BHCOr_~_}4J- z&-lRah>Ab2`rpu!!}lZ-YVdd)3XP6?W+bFn&k7$#ieVcv1YB8=i+K?J1FZ%VpaO=g@=6N%^y5}gjYmI zM5OX4Tz=i@B!*Vx-oA*h70aURc$slVAFb`1-QtDNFTs`kn8Hk%!w&d&bl`J7)(%s6 zmYxh5@pzw80&&{QoF06}fJXBiZ^f(tl}!WApYTgQFY;fb^6dYSDeN!LaWcljFC%$oczJnz{jUpW@XRK z|LbCTiG;HMp1^1!H@?}|`g^sWs%j;pR@}I~TfW;m4?jxnHa0NDSFUDmkfCGKAWf1X zQ!`QoNv0>GIU(C03TfPJ#~m^E6LA5W`@fVXwJ)=|;u7$<_}nxjP2S3;lJ4`0ZFv7{ zi`k4tLALAb^wlk@aZbZn?~o5vSZUDP6Ze*#48ymc)q&iuq3`cQ{kfUw9rfsx-gaM~ z^sHt|`ZSp&?&@wEcpkItA)X=|;m~--l?wBytUbChUgS~o(@4z!QN4Q-Ot`rQ@ zkCFZ?9cfpquWZ4Qb zQ(W{lHIT^Pxmg<-DoBuocZ70N6%st3`$)+VtYGZDodyLrtv*hLW-gWpu`DuqQgD;w zKb@3~V^aAbVSiVSXgI60I=pQ~q*gsh(_rx4{?s}p#k*sxe7ajR)A|vI513AsF3;0% zjR9H=rhlS>@fW2rE)&r{l6}2$-*5SO?uxR^eKlfZ*mDfp6HrX>o2yI`4)`XyBD2-< zXC+fLJC`O~ce;^17NgDLCco>TVc!pL5rAln8T{-ZjHMNW99Wz9S_$!u6hH z_-Lw&h83uI*f_A)#u?OTMoJZ@4A3-dx%AXFU`k{r3q_b<^Xt4(KvJtQkqUikjJiLq zIjwz}@)cPsQ9$unz2<^Rc`)vla$$9e8L4`ucM7v`1x5@rSDuu*v%EmlCOpI>a+WwG z^K_Dc{!$tcG!G-yVNgy9B^Z?=Vf4gFSNiW&Xf>w0Yl##0E)1NHlnZ(^Qk{+s_G1CF6-U{QUrcu+5Zb_$?qYgk$rRpk9SsDx9dx2 z7c*qe0DhF4O;2Vem@;cmzWJ9 z!o+VN%dJbU;XLK3f@>+f`vu1+;SC-2%?U@&9AydqlH&dPI707QcGe^h9(iilp<5%G z2+gg&Rm*NiJxMEF)%X+p!_Enq3B?=a)$Mxft@>9RDS*He?F*r0XX`G6BT6dm`35Ea z$gyYsy>lU;>UH12vO7V>nZ|f$s74A!%Gu}n!ehn?t^&HVyB8K4`gcdTBdK1L8B@AO zpw+0LkY?_64Sj-s2ON=+tf%3{$6?EF-kh^3_Hl)|032BA=H~K;oBHvVL7|DquxDQC zUwg(+X`-4UE2DKgtT`VeAZ8iz_#>#*MTh5|IS0uhE~i2lA`vsAl+o$axGJ*eDUL3% zf+vbE^xp?x$SW?g-57VVY}akzkukld!S{w1#7{@7DYTjIc;(paNgOxf3@nnqBzsc> zDH-=*XMp^p<=q;l<(}b z?B>^EHw%i;rxgZrW2B2f&G=_8z4sjEPstz}6#7)Z&`~UCeBWhnC`t`=C)n_*u4NF> zAMZLC?E4{Pl~;#h;j^=A&KqHjaj<44;=YM8JYPU2F&VHGm^GtEx;%B2OJNgxyGEGB zPa7cuTPvR3A~hcqU=kjs0$tFLI`7Q3Pe%tK^`zjo;(Q^zMH~)^m`-e$WmV*@7?Xye zS}~h9Y~2AH)J$HLJr+|a*^yh9+-0?;vOlV#`VLZN2@#43hf>J1Pe-I>4OcBhUR68J zmwDj!1F9LDgmV5{DCLE7$Zd1mO&wc}YPIfHe21Fn8FNUu8Vj0Er(9$xy*oagAM+bX zNTVmJh2@ed9_?Tg5Y6n;f>K3VW(PlLBZ!lpW!vZ9G^x}HQ>P!5IV(UI!=kk*LWgr0 zzG6LpFQdPf;+__8T6FA^ZBBAz9SFeYH z&By#?OcqN`I>+VC8{y^Pwa!BU#IT>L|6KF4vW{vNqxE`b z2h4atNPQobe!g=9k8ajoxr1_J>j+iH)Q1CTt-a&BH)gzGK%toWs$>|i&v-yLr}gNV z5`fJdb$|fb(cbOJAVt|ofc{kVLa~bNeAZ{KAkT`?HoGcoZ#;vD#mJR{$mpbPO`*nJ zw^D~8g{X60`q0fZ}WRDP#?);I*^mpuvUr;9vhx3inDTK{EzOaYW z!WUvEH2!%Kpg5@JSZ)&B#(Sx@ggjmS(D1kqPPYNgBwC>$z;|jOJnk!=<%tkPZ`xN= zL%c7*RI9?91;M4@?HSdb!hDu|KOqOrT`*-sLfR7tCjmxQm+XL5yHCmz+PyzKl7F`S zDUbH=$orQNtP*1$Wve05TwdIieBJbhc|i6e^ zKr?MQX;lCK1Z~C*^h~!vJR9N^92s6aNnCsJ4RkXdUv=y6#khfyLfJfjj$yWd4^|#1 zB^Oq(4A2wZyu;DFR?ESx;%aM!zCL}4{qPFRZB2;I(q?hbU*Of9YT>j%*~rS$3g6UM z#u~3)(FKNJ+}^|3xRutlN#^Y5aPCT5RC7~w%oNuULamhgKG2)bs__d(JiAV{PvCZG zf-fcM2-yjX!kS{47PsJ(VZ`TyVl{Xyh9oH)U;AU{ZWxICS-aC=;3rSQ7h6gG&h%w{)3p4V3%$X?>Ck|Ci9 z1Bz@C*J>wiMiP(m9$Mn%ZbS)smo3|K=1a}0kVn|q>@U$ZN8Z@1ryDFigN*3uOW*;* zh`ksW_zv|nRYMMtOIiq(F_ntpUG(f2PzyV3;EB^KoLsNdyJ1WMdkVmN4OC=lc1v|O zTHMr18V;n?Zzk-iQv32{`PvCm30u{3i9&-g&=hAkO-}&-iTPqX`}MP{Xl?lfXDrOA zJlQmJxQl@%!aH+j%y}M?VHlj(a&PXkkiLT8^wMKEke~P6bh*>2 z;r$f;7{=HM3k?3eH8%O?9{i9(_Q`&Muj$viVdi+jX!h;`yzN;}8IrZhs-Qb! z!zgi=lNj}iNjmB7+<2q(+t)cG?>SPJqinLqWhsM_Hlmmu%~K}N9aIM9I7u_jhv=%|j+Poj&5m+pwz%)N7iPp2_Uu+lk0E9QFXBWZ}9xzY)?BvCjbV zWFIdOo|32h@ddf$H&n}AzyddiKI3xzd7Q%^1PNzD9YW5R2Yw#zq`zeLQ7;s|gWfw; z%u4_5+k`)B$$&ee(5?*^2QLpyfQorQN}5NE$!*X#&_jbWAhvqik@D+t^G`~csA$Mq zETFaY5c=b2y0R$YZ$A|JaXeZA^#AI4G48!HeV%hGOaXM&wLr7@tq|41rQO*4OM9=u z!K%N&tNG%MZoDTS3r;39AL$r=bZ@ZfDp*nV5FPaz%VLeaWlC;sK(P#-=R?VhkBqNp z?27g6^2^i@*$Ra}E#?YiH#N!#a~t;htq*e7{u7;mf&TGBsb9Nji2nC|D+`Vxm^w72F16~V0qN3t#JOs7zfn8a#FSpNoVV;f2FXVC8axYMESK(P@^da|&=GFyGk{(+#nw-VmrR%{^ z*s3|&l@v;w{TAi@d=tR6b`2bKr&UFy#Yja;@-uJCfc^5j znnkXJ?U}4*=iQ}HAqWKcZ(_&`rs&kN&=Qzh*olkF0UPzl*yJq{mb$~ehq3M0tMRYq z-RZx92$;$$s|G!Y(s8U7SeUN)jzxtai?+e-*_8L;LHW=dLr300Yhopg*inRq$09S} z2)lZcK~^E{MF-goSL41|dm2io;EB_D2r`@Q0ZCQYlVFD(R;?|%;UfTn&Koi>ae1$0 z!z$BX`cje@M@op$<3;@Z)q{lV78N4*#&&IH`7b>(@xuW#f?k6QhGX_b=(e`Z42iv8 zKCbN~dtPf5=wQ$SZFe7wkoa3&M7!sStFzSZT{e&55&Tw?!o0^s#?e|Fsc+tghze<@ zBg7Z4RmuzM&q~b%?|ecl0!%h9udl|w*o&RO!QfsM_I-e}!| zCtcg5+oszKILi=Q$Lj<~pd_sA77{vGnWU3Wbf7sZOm!A#m!f#HYJG$Cf3f%8VNLGa zwlIo{f`Ez?X+f!ifCxw@A_4{kr1u~q(nNYMQIRenRjHBQiFBn(mtI5f5K2O?0Ydy< zT#L2cYkT&+_uO;7``qWbe_)aq6Y{p-oO8@E$H>8j()=cDP%!j${I(G~3vm1AZ zK6Vdv%YufR=cII*XFUIHL~C#(dR?i=TEP~U`EpZxW{SeR9MDn6R3^P;~Ggj@u5y9JCtplpN4pSu8srvAu4SYW$PKaf@vDlSJR-o*(!SULyZrq{5hb}`zfo4k8T zx6b&m6!RJQ`+(@Q?mQ+ijs1dfi-ntjZHcRTl907pcOIT){2fUG6F4il75|pD-+P8J zj$_o;#GT3wzkC}H>#a-=>EPp=(AsA#J4%~6Qa*a2SrMwnT_C+5g)jE2^P=b% zDZ6}5V(JU~b_?ZPu_|i016&+g6Clrfu%DPPsL$QrA%67f!$bQ%T z&=rhHE`;40WVX(l95G(r8{{)ClyAcYTj*g28>p~^k4+iFZNgNr!vR?T^Qfd*!K%s$F+Eht_xVc?5n~!=j!|j zaABXR_3NIa54BjDD=h6_9_ih=qG9W+38fozqeZLvcru1lz9=s?ZXX=;*#Aw)bB>*? zJ3Msvi%g6SYBwX9g(9(qDvg9dP0Bm zK@f>3WL9HiaX0+H{F>pyWAzw;fp@=4AukKuZd;H$ECUqyl=Jvc(#4aK{?|O_*P!@c zm>&K5v8)dgMJRioX{i#wCzC>Gmbz4$O!HrzeTH(XmK%HTV4VR=x=|TkPJZ4JJ~UFd zpBJR}j-}eW2bgkId=%A@*&#E$-0mL46{i=#6icnc{4|$uOdWcXof)h(aFbsb407Ct zYyghXW!t!%uB8kInTNvi^}ow~AHBw(y!H2q-LK#Lzv+Pd2Ok@2X0nxfGzuv4u@A)j zb@mlcoKyZ&sl54mBU9oX<0ph<6Fa2;#@O&T^zXkDi~il~GrJ!mhT8k|AceAl44(jouGs;E zhzbWNU!s3C3jY;M2YC5^xGSlh4Eg+xg!Fe(QXs+2o)m=}O@TT?J{k-ViL8<#9Jr@? zIO}c-=R6&hLIS8GL~t@}%lYazP%L5np3rN_ z*~>>Xmv@cs7#UMMCwY|B?7X(`fWw5RQKofpL4;cWV08Wprk(wtV0QirYFU`k_K#4d zf8ax*(*K1daNLjoPxyvfnoH6l9m7e}id^M#dp$D2z*+OPEbo&9na6s- z%#az>7gKIs7`}4k4sYJfvU`Y0dYxyeox`(c+$C^~tZ95`D<}y!9(%os=-;MXl)Q$0 zd85J?qY?k`=@a||Lucl62xdFW5yW72@W9ZdcUsR^iSjAk4Z~)@UV?U!TTm??*D?xgsnP<{Q1!d_2g+lc;~#$fXG5{mX;*$PIIp$o157CXk23}SIFEUr^klqSiKK=CPq2w6HN5zF!9S? zejS}B^|u3~XxaA8+xqWXZf+BPK9aDob@5Sk3Tb;_%RA)oQBK9 z^tt7NqcQLdFsI5{zabRDT0Lo+RwQH9leXCFDM5tM9{E8KfN)%@aq-A85qWrZU7{;1 zS9e|o-5G;~*wsIvT!CKgd$Mlroz`#N)RL+!>9IqIRRh{QiZ(EE4~e5P#H}s17t0$g zG4%W}V!X!@QI(a4>IQj&J)_@CpxdJzKIM(9@qwM{nvvk@Y7~NI*Ed^D?i@LY+O(*o zW7>M?8FR9fbI>2C_t@{Wqx_eNo=Cj+}E@|tk9S(UO4M1b6X| zRC{xN?NM?uzl8O@0V{E%eF76drmbFwSs)#FfhLk>#aKF6_(C(rZiu;Ph)D#T>9{NM0UxEQ)O8CP#hnL?c zvTZEoCmUjs`Qmx1i>JB;d|u(mEO`Qn-=RQ0L)()iIQg*g7eJpa;%)f|!GSQSYilRJ zO*M92OwB%Hq}k!#IO@XPXvSHw7k<6N8h{{8O; zSq&=}lY+7!6Fkjopav@eF&S4A*E4}mg;Av5V~kQqoQ5l1XVHcjpF}{MOCqln-X4n4 z6bvOdm*oF!aKFxMU@kP=fnMm42iyWmbA@K4O_);y!(SwFH@3TE89(h&M(Kmus>sS> z1`TOdp=R4%SlLQYmi-H>J<$HNeHSX;D@u2EIx&9#?p_%mXt(YQ{(fa$?kHMl;Nzhn zszDe4-0TW}5HM?RR?i%)GQt{?t92R6l5vu~+uywnZcOS^By8qiB{ylI44ws_)2>ff z%v|a|O4OD#zoE8SxX0wwth-prKa-_dM7dX)W^@#MA43T3t7Xb_1S@lm`+UIuLI>$JLx%nOg7tm?lprB0zG$&fb&9j*yf zjETR`h@SdEKyUgAMh1^^T`fs#OR*@@Q3$_;qC0GLqcxz`_#lfkstRSfS&!I@+yNF? zzTV_P2aX+uTfloirolq^TR?t>ohj3%${XhUDQy7Nx9H<>N9#ZO`Gt=4S10Bl!yG5} zH2>D9=9g69*RFeKo zEy5EqO}9U-0{*4F^m8KSs#;1gDhzOk8xJ|1*}}ebk2JY_g?V~616Hcgdn-vI-lPjh zR5okip=;{vas5h%m@GTt4M@m7IUwOU#i<{*sM40*VXr(M>SPlu_h!da&vGC9{1s5* zLa^U=8I(8kN4n}HLF?P6F)Cqf0Q?aBf#pnB;WpkeCDR1l-N>5- z8zv0L(G)QV9+pJu@loLeH`U_A`H4XyUp(!?2j{Y#BmHe|y%~vOg8Edw{RSJ>NGqvr zXvTIvwc5ZA!&Ak6)@`gtTZa5b8U zTUF&>kUNlk1D%C05n}4k=C%p9DF=DSdr|0i!wNcQUKhg$R?fPCW8S+C`}0P_zzFUV zv?zC}D|0wKK6Fm^uJ}y+74Kx;bWcyBDvkA!H`R>Yd-^_lF>)~??5hVno_=UWdr9_< zd0hp?>zm5tcHnSWC<>#ta+JVps_a~GbTRsxNOno79<#Fc235?o0$;|`deb1Wq_)~> zMg_>Qe=yb%O|6?Ssz33lSAM+fo&>=%XAvOq`21GGT(})1HD7~{wm*Ae@#^BN>$8Wm zs-1J3ne@OcxRvt{f{Wr3yW^0ZpttQ*elK+B1rlps9X-rMXsD#mACLvJYg(LN=1wWY z!lqO=&szEzbb=RBJGwoIls|R2q%}5_Io}ymLZPAnVoPd7xMdMGVg|ILXn80~)R7m) zY2#uJ_Qx}T!MZbCafFa2vQ+#{3+s2;X{<`1iw?vde@{aUnrVOp*zER`Sf?s>!ziB7nxY>B8;0;a$jq7_#0HUNc0S2}VmD_`Z(Kq$K|+G7 zjjLciG?b_KoPt8f9<7sYnTT1{;OOYSR)Q9ZsOs()Pmo4W!uAGV7Rc1_WoWhtksP4B zDqkLd=W{Jlcc0}5aJ3RINeX^8P*zA}EjMXZf;%ajGFBD!dqvHOhQzt1PE;jqlU{5= z>V4TE0y4*UlAM%LrIrpxhNPT4hxsV^-tL{81QJz+3j46$hb_~C-2+yX$ZNtCd7b7m z5S_$|oTX^VyWPeWg^tvxAimQ5jEj>CAvK0Mi?`j{lsX9K-QBYW2gNfX$WH-OT-JBu>VuQ;@!%%GD4GQH5k z>$B`?aP^WD)4R(ffmybt-cABN9M6_J6Rg|Hy&wqAfhOX)&vAm+ImnJ4%O zHI3dll8{4#b+dXD?fa0V&ACdPpdA2ns01fCRTFH0hIwSe02J>_Oc5c)9{uhCXkjP+ zum2%%^IxnI&P5ZKcFAb5@0Kh&aw7EM>_ zPBtb2x-2#IV>e!WWXu;6-oqzXj_UQY_Xt8>WGtAsN(wtlkaASNTW)?F@-fixV$C^I zfQv2{`)eI}{G4(TOzzn3*OO0&oa(}h*9>kUB!9Ml$^(x_H68(@8aE=eWcuJN`BvB( zX?)`VU8}xs*N32PfVqwH&-bdb*rX9RpJolelx!!Iad?F+XL|+f4OLp~sbJ31RFxvi z6i8k*Fte-3`#8dpoZ0aI(t|%zBu+g`*O#lwr9ObF<{6b$3(i$M!{WrVUW-27Hb!GV z@C2TD-xeFII83cXzgbX~Jti|vem)@1je99(qAdzH8!qYeorK>|mk@a?yRtMD~1i6eXoiQIDsb@^1+*&4OC0JpRA z$=~k&|FNzJ=Wap@nzp#wAUHRIe(E!g#zDllPnnG^Bb>`AY9qsAlSo~w#Yb*c0PHLiNv^gV$1{E{Pf@h17QH9V`7EoG>^*aL?Sd72s+FJ+1m6}UZ zg!xvpN6V9|A(H{$aF)bXe&1+s_ZFXW=A3bFe>jY$IA(=f%0on;dN-D8TQxad(3Vw7 zmN2rVOsHH#8O;64&R7ZK(1Hm0=_C4&8e6g~16_}^7Zoq#;+(8^FT~|BgLmmJMFv2-rq`&Jy!Z-5p*a^7 z95B8<__^vuchJj}65B-Hb<)na(A{RwriA`I7udv4m_g>E-u$ixG7WnNW2D7foRSM; z4$7~ug>s2LnpLiXZC}$v83Uxyk9!9IC;`v2-PX#GPt9tPXNRlCTgXMrC1Ejjulxoa zw!1d;Wor&-@HOx}SCkw3YY$Btw%|-jnD2z_8om|}|Bz;a9jx<%#7_>hA6(4a0(zj) zkK8@UfxuZ_aCKHya^gdWlSpQNBUBiuT=B4Rt{8;q!54H3 zelK$mvy*==@QZpR!;BzXsYD&@l$mJoRDUcLDbtuL`^Y=D)!CZfFFy!qu*8${eUqxa zBg&jN#1~maILWUO;Viqwh4Q3w%?1XkI&*6&P5c>EUd?G#DdzHWr}pur-P=4TPzdS?LQ^K49y%OD5E$JnmC2BeQ7NcW9slZYrQfLm7ixWcCsMMJe`aLy6@QOm0nW`!!*u4H->x1K$)r3s zMlVmaUW1s$)$ZhT(9;L*coOe|sIX84h5B+6VBQ!QRVTs69C)j6Gjs-e;S7W}#ew2J zQ@bO5vcpukA?$A78ZeAWkF+RC$oZoV1H#D(l>g`Be~EJL-}dZ(NiE>tz4yOEA&g>J zR{`cuno1I5Vv~axL^Co~1l3hjgmiYVL`Agt4T2kf5VRjX%nkK(yL+a*vJFFj4VX1P z_DAf*mg(S!2j*Nx?{?!?dG8acVc~tpm3}(7BBKNOQ2-#fw*NhE`}z7$&y2nkg|JWO z{@q@m{7x$k%LUZ=It+|BznDwe$9iE5qcUGhF4cuy3+eQ|$1@rresizA{&K%e_js-etI!Nz!2^ZFDePdPh3-H~PD}Lx8shr>(X`eg5#{|5 zz15q~FQfC6`8%R^;cK@dv0d2pk{SVhJlRMe4OWlQl`DqHH0 z&Y7a6xx`AV-spqg)&=5h5rllV2dl{^siTza7K$;ZF`dvB^1l8#KJ64V&#l`={>J5;{m7dZ6J_iKb?wZ1WqAW!05E}~+XPCbkEcCpu1_WHR9 zZdCgOQ_b`6`|@?2c96vmAdW70m?K~>Ft^@UTX7eL8K*F=V1E!WaTRTNWs&=di%`WF zZz^!Qs=P*CveCngqU}d2lPmbRRo^6mV&=M)Qpe3b-T+HJ;b$&6>E7pqfXyCZ`97X~n6#TM)C#d?+9lB;fx zY#Rf44WwA^ zgk)p9A8UPfIh3)IliEu1NiQ$6X(_O4R6gSl7_iTsp?sQqo*58TET@dp9_u4D(DV~h z0fOMrBCxFgg^W__gn4sOnuweMGUz_s^^I%KE@W7)-tkhecP|$m_s{sW)al#Q$(C^?d9gYm5Yw}g@3PZHl`m)Pl zHURy`_X4WqONa}L87kPDi}$hRaQ#z>_^;7fc=iaWpD4;rzzUmFuZr~oTwBL>?ZS=T z@BScob{IPFnQPD?SiZSww_Uy?+OY9ztNX_ zM6mZegY-k?H_F3F0`i{gZdQ_^*g0xoI7q!mCjL`7lvCFvHMzGN}3dNKFF9}KXT*4~CEQb}}goy0I>I(#h7c360#)1 za$Q_!yFRVLPIYJ?WPt8~zf~G?;qk>+sVuuHdvSocvq0){@Xh)}O12KA;8S70XNN4`rNreZb?8*~Rp0cT!F7e_cF+C*4ct|= z004k=UumQ<*5e(l_p+Sy(OPqrjl)m3F4pJ3>AUmuiwEA+&>2^`TEzE_;90~yGUc!A zhc*3bT;-p!=9yh)&>1wHtAR_<_wG);1_$*RHrE0Ai0VBf_0104s+=D{w?Nm z5hqT~Jt1M5nQ(I|;jo0EX$@Hjjw`=ck`b*n`mEZ>khIU zZ!LvsZeE(?@>I5N2%IZ{`RhilE-tI<)VYk2Es@WTuH%*rszO3@j6MVAxBSaBh)vNU zfVO$jMUpl8d9W$rb(9#&`qK*bls+D@!RG2LlRE0Jm634!Y|Yt4F#RCOtwKd+2v>19 zUmHJt1Rz*zA<>S0u2#lM*#hEjqjoOS+KS0vWr>x>DGlSQIIFH|Fxp#Hd`|vFDM2$m z;1d*6CTURkR1jPlQ;miN?B-dMy4;!7y6NazPQsH?1litZ+(BX_nfpAr%><4lIYy>s zsVEN%4hwd6(H8pU%r2^Zv2Hial6pMI)2}fU@xP;DzVpbFU>Pi>AFox07u{f}icf8R zM(2Lt&qu01Vr;jo%3f%1?-E7TCpe-L>dT+Kn$E8Fl487YV3Lpi&AHIz|aFYz;LAd(QB?zrr zPx;#)?BH_Z`c_aIW$#es3ChD7WnUyx=sEG=8B5CIv_yK6XY*o>Hx zuuMZ0+kPIxE{MmH-3-7mflH6i7I-jx;QospZ9 zz)07LzF;I@_b89T4hsY1L!xIlFk88_4T;}r_pvpwwSsX5Vl%bVGw9qc2gwy%Z2KyMbU zZ$Fl?32$QwbjrS5K`a_SC^5y&DsaSU00lJqZ*uBkpVBXEGB(z) zG0D&PXJeT6O$_(^V9o0YmMo?(u>&Tt7oaz_q8wga#v^BzMH@KmQuLT5RYaQD4fS%PS)4y77v}Q%)M_FV;9GVPTD>^XT3O z6pTeu^@R9dKnGqR%Dk^Pdq2E@@}{|t`8cDZj5P*Ijuf!!rNQ^S0Hcbfg&Aiv@z9;u z27K|)5L8b0ZDT79>H;~kIZmJiF3b8v_I`0e=lPLptTuj^2+S3_=!u~UjH%-fy%p7? z68ULAc}Joi&jPw!YzUcUtkb6XLBMwaLMY$d0Qu~YlN#)6L8{Mz%fF~>)?h&vU41)r zRh&CK%Nb^-O7E(U{8uG%1}nic9b8RL+-c~~4P{DSTtKUBtA2ykq9MrnFa5O@^%H(C zGqF8P;jRfz!&hw7++oaQVnogQy`=aQ+& zByFOKpW_qvQ0SLNtBW#6t=S71ff*P2v;!5!-SgY)kT`Ibnf;6EG80vsLIlU8^pWU* ziCbyd>axE(`js-ga)f@=R}=l z?NA)s(&x=pzMY(hrZrfNvU^fp>hf&mi;ZXPB8)goe?>5bA2>emPhb$L;?e!$Dp|Vk zKmr6T)uYMt!aoDmmyG4l-9K}fvN_IJnJX$;a8nOsk zK5CxDHCH|Qpl&UzFcsZC!;zoSObgnD=Iv&xQI$RT+$`fY3zV0Dl3aR1dm1(M&nS5R zZ{eJ%<4ZS0iOhQru_2!)!od0L*O{Mss1Y-6@SrX*IfNI zm&zmnLf6U9skM`-HVOP33>e2z`;QIGNXnHGc#i!_sdebi)|6KP78yO_CYg;%ST~6~ zeRHl6t@psbw{I)Y_H76nextoVywr={Bj9>;{{Cb=4H|X@EI)&j$tf+#Q*?|#nLfL> zE7iA!!S=uGj5;iLMz?4GTge8O6Ccwk^IQ27jSC7ov6sHZoOy=uI^y_D3EW2iAuZhk22`Y=E#_w zIGL*U(4Eid^9aqNuSFX*z7yBJ>AX694-mcj3CT$B8FYTEu>?Hjh5#1EKuX4#x%1}m zSpG1x@Gf%!V^cOVzC%y;ILuJ?xZG$`=%Up5{I=yP@BBmCx+!`J>`ak>CnH9$MnOTN zMZ5xD9)z~+XNyxw2vgn^ISzZX`q24*}cF1%M}7#DPPmgonxzpN{jp}5;H!jpp}j8 z^;$pB2oZ{w2@x=L64p=&S1xe_`QpeKvh#KeDHCK-ui#Mjp*49&xch_EqnW{Xh}x+#?ii;)0Q2*uoeo<^;qrhIdAbL4^-`&5 z1t;KAuuYIMu&)0>Kn{Cpi4kat6BCE9$z7bRtAT^>Sw=o^eWu-`Aa*JH)3b1Qtfmc5spI?U4pgs~`pjsP&ckonKAhYfm^=#{dUH>JwW_9>>qyse2N+1P zS#KH3nmUq`#tz>4JmAqk9@8Su3KwK={Xsw|t+QU8ksRJvKYveDnVJg=4q{$WvA#i|mP~+A(S# zCCJJHMm-q|3p`_gvDS@MLe#?PXDFaj*~l$Nzl)6O8N7Crs}VpZAALH$(bDA1#tmq4 zZhn7aBbA!`NV1W?*?C^M)6QJPnH1{^t52^QUo5qFI zsweSUk;2rv7W4O-p%jN=k_aQvs`mVh;PKFxi@VzajE6YELN!fNhdAnCf9M?`#uo|z z|NI5d{fEgA5RNZc<(vF6Mth^m;%?E#ppSfMe_Tzj>g&O!BU0D1FJ1HuJqa~*AZ!Er zEQ?~HDy!h;fpqx=C!#A^kh+XoZqR;=P;`X$c$=k-4_iQBAzZf~(ErFbyLGnsaH{Jz zq!pBrF>QbdaX5n%*co_n&(-)|R}uV<j&e)MKcAU1G}T`zH0y3; zEqWbJFT>l$2`hKUgM#hc*6QOh+VnZg!j7ORKY!WzKM9h>E}b zj&4QUX!xEN7+GLCj*T~azHi8f%QhOEH!L7`M?tnXFKwgc%F5`gW)3ei`BVJ>0 z+(%$epOk_=Kp+vGJvIe|mVYI%{vRT(rWX|_;&&%H2f%k`W@G|czSG5o5VXu@WtyGw zes3c~;9P)}t8?RJT<$RjhQYX9S0ZELxc>oi23)m zKzIGztv~5vRI|Ag`tz=`yNsa#~8G7_{Dkjo{WTrSe$q@@pA78&_l7dG;r=AqrJCW5oZmdXk~=39+5 z-&hb2ofkvO6L93i>!D``aLN}y+%3(Gei$BRgVNAJUVB5@&L4hy&U0*TI~N1Lh`H;w zEXvyWE+DhLwV?jJ!pp-iOtVCT0;eKm90&1~-2%ujrC&FEz9N}4C#@+XL#aO!ja}L9 zbm`=aO_{DCNwAwZz^@)ih{=D}5E8Px?m;E2p+R3nG4e{}fGc?r#}f;UGtXHXiF8Yh za9FuAmMhB!p$-r6TaC995uxl#23%civrL~-s$UFOwji*goPhgLwPdpS@do5kG=#hg zA|#}#4-2~%Crldp!t~uB7#PDNfm)WgAGkUc={<0@W`|$5>XF>A&Uob3eU6f{ADwum zIOu+yZg)v4GB+FFEo0}4V-8aT?PMkC35UH-i7_+__jl zk$Wc?O2YD;Y}g4@my(rxuswNt36gHsiyqXLO;9*zJ?(D~2N0B-kYQR?Kk;1N5XT3- z#oEO=`plgbcM|aKLIh`v*zIzZf^lF3tshpTk5EB2cMn`46)n`C*&__H)W!Uy#;T#Js3=r zj*hevdfw@;%cT~8jYt_aFYl+m+mox{N0c&sr)BU=IP0 z6HMO98f0w#LD2768PGHP(>{6oN*Qa60rFa%R z^7i7e^DwDJaFmD`?UVJy;jbMdBu=0Wu64%ONBer~u9F9+4m{oVU*=?wTT-t%I~^u` zAr@s?CMprr9^fgd*sCai5mCo8!KlwRNts7nMk4I8T;!Pu$U()XLxzq&Vm;{dic=^d#|wW$y(M-*!;!a|X{yXTfo( zg(&z7qrz2hys{i83kK+|E+8 zvE-dAqvcQ7jRmg(hbFC$%IPUedOn{zYySFSo6}Fv@ia?W`mKkyS|*dU27>uf=F>-l zwh~L1l6lvgZj(=Ud-5w}=}S!^&Zkn%UEAHaWn8t=ZS;MrMH2TW{Vu~nQlKRt5+!zkb^!POmk6eXmV)8hs+Ols6M)Xw`}O|$xh zpTFQaOMwW>4qJF2SF31UAg4Q*ND0Z16?dOUpYQ|5`3Q5af;7*|qd30xz0n<6P7q-M5uLs#RXL4tjHC1nD<&cmHwX z%5@ZceGZs+fqVqKvjE0g;?9CmM1!V_Uttde)^GVHCy8?oAOnwhxOIG|lhzO~HtP+{ z`1HpC#jz_N(8P1XDN`anqz4{oZkVm#C3&@FTo_soR1~lBcu9xqr(wh`o-TJ=mlhP! zMC*t_;3#}VQ}LW zjPxMNu(0*afy8i+H=&4~AN(MAyv?*w1og0>cKG<1(OYi^Wo+V4 z$O%NA_cC(KoZ`HW?}JCqvXj#NUd3{kX7(*|Zin1#+#Jw^y`Jya=#W;Z>dn}+wwj2# zi?mZ$xK5XTcwoYAxJnPu=NLkPdfj*9gHPOp@q>)^X-Rg=Lh%N}X*%`C4w!aDF z$odQJ{fA7oIee_RN_MIk{ly$8Tv^CV6<`nc!TNN^_3Gl#mVnjYWS@7442W8oK; zbWvt!Lp1mR+1||Dc%y}m(&2g(z{heB#JFj!Ac8CTSW70(Or7~imOUO11GlS#9mjUo zH?_;bTJVNy>I2}`oKx4RQdWY?_p=p#)rZg-OMG-<9zx0&wM6RcDcb^0iY6?#MF0WO zw)+D^e@@>0wsZF2&nT*D`%33)Wj_cOXFdd(w~iGe~o+sRNq`IICE9< zD`g#~nh+@v+^2^a<(3nhHH?uf|3RPzXRA87=2yH(3;isY@2^qCQQsV#9Jvgy+)%)*LKqw5{K6t-RU~C#O%}}BnHKq zvlnF&%_=cQy)Iu3o4?>JMYUMK@`KHl!*!SWwfk6m1tH66b4mMphiyZ6 z4cRJi6X56D8N`@l8o=Ts-6}*487Z zbs?8Eps3dsoICL7YqT|HZExANS*8)iZ(li2KbPp{aH9}sjacfKvE(HmTBLoRE`WVn z_;$Z&3NlXd!kdY&Nl8hLW$Zo*zsB8!#R*77hSQCxm)-Cw_5h^tx|MxVy9?Cn6qXwExZIP%Llfrq?O8o6ocet zLYgLS=*wo?w}=<8#77Lg5)D38DKs-l0hw0D%2};kkC%`Buu%~g>7e;!^NP*zh}~@> z!8l?X{CNuwIELC#Y^82R>}-za6;oiTB^&_QRRVEl_-QJYTTH2FIM<_T|~s# zOo6!2jCa!Lq_)9ObLNs3I(fcV?q+xfXKQ`b6Zh_OjURAfR$A?c@X_1*IzWyUZ6Asy zHF6OG6x-fTN^KROeh`?Au68|g4N}A4ITY0ti+crD?n>}ON}QssqSd&>gKM4bgPZBe z&Yfjs=`U^p7Z~+C4dgd5xzgo$6$)e$Li~)UgvC#Lk`h>07@GYc@EMjA0t)caNp=JP z^MBZGblBSWf;Ga6jN8Zf$_}gTU8B0-{=0}wwy(UtR-gv|S0r0v=i6StzV1iE9qM5k zuXS9hT%9n}?P%P8P^nk!f?$HPzPyZIw?^n6bf24i1D;dcD2u_T9E$KK#^GvKj(6v zUGm~73Az^~J?Nm0%(X(9Tfus1FEDZMy?R2Xo@g6V<4eHn(u{wgOy~;7;9G3tcZTP2 z@N~N@jmdb(1Fl7hBY@4hR_}snRKo_Y zJ$)I{#DADs1sk!3p$u#TlfS0bNUKLuPE60hDOzT@x@uT7ZQ1j z3ot9{bzZtojPHc z0}H}*l+NFLL%sm{$0^zY(O_k{pHChdLWl=hoeMJ{gZ;1)KtugcN#NaFw3@?s%$=Yh zhJU>2AJ=OUi`NZ2aEKWkaC{~X|e>e|5fL`Emp!0FTYn$;)_-&N^+g2 zY__u4xkk90luF&euZzE~+T3$uXa8V7Oau&u-u5L4g^U>5Fr&fvc*Al+p8z z$on$P6OafE!wa0OEHN@V3lU5~&){N5ldGXBYmZLO$AvwX9|X<^fhGjt-@NI6uu)JV zEpAdjttz|iR=!PkCb%hYF%eWX_|l`|+2|Gc#G`tY@`faGv4t^c%9G!ozI`@&s)cWx z(R-VcM^i8Z(x#QnWd^1AyuMAO(vJd^>b_L0hj~gU*Pyf+SvHC+uCEB}g_Ee&Kct?2 z@8v;0xUb1pHKQ=@=FvW3P#)iW8XoBmS6kEG=GZY&$}YPlAieSDQYm|K*_lUb(2$?( zDoxPg8lYMlx&dgB($Xq@JZ8Q$QF-=BUKrk!Q+32BJ; zP~wV3hiE2|qV^evggGXPcSGlGpduS*-0;=&BHAN*&pB+q5+Td?z@I4FPrgr#j8k8F zR_%Q;=J3?9ITZKMZ6Y>UCSx#49C91(s>ap}FR6QcW9;z6|)cyNNcEhnyo zGUjn(>b^z37M7mp^?a2FXN`3P{jvPGx*HTu2|CG7q_mtmFF9yeeOGf>iq zvXR=MAIt=%+r{l%ooL4zAbZ9YPYA3{7j|$*maS&FT+`FKcf(3(w^Fzo4w#|qE9o`q z5JDaeU)hPw@fgcGN6#Au)GFbvYc7r`ZFaIM|M_y|b2zE;ZbmV&8NlN}YJ7%qB4u|~l1^ww4&niJqGn|ozk|6R_IyZO7d z?h{G0(AHAfjhWU}GgeT8H==rcc>`F^4jv8+$8kLOB3 z+bFB}!fhaD%8Cx^y;AlV$QZDLBZWMgA!my35@l)#-@pT;il-_b03m*Zd%Mp*=RNQB``-3Pt{E|mF|%f^ zXWh?T>~l9+W*a=r(e)NhK8J+KI1ZXG>BJphG$*&jK~8(|Hwg$i=v*v6ZGE_ma_{G)WP&3o?vS6yQL5xk&qGHx_h-n96k#SeZqPhscvbt^ z9^?<0UG-=U7c7nNQ*XZmi{vM`k~E>I$dwXQKj%P_pg=b!Hs-fJ@E=EZ#usaT>9HZ_ zKZHZl2_D$g0R7U`Cxusm_{WG*Xr42)@nD*|&;qJzj9g*)@ecgp+8_I=t(hxWB~nsN zdM&m4<>J@emzO@kum!Njik}3O*fPBv+A$Biq%&J*?(OlMRY2q;R5=_;T6K?OioFV>CNH06L>}jaA z9tE0gIg7qzIs}#t%<&$O4&Z8PL$Cir)ueMf{PEE((rvE#-Q)e8wM`Mq1(rc39q=n# zUi^q}A4j z$v5atcTFa>!lWcJX`RxV5$c@8HDK4G~Hq>E%_99gs zJg;F`59F6e5pW9SawX^BT2J5l<>%2Vc1m$r=3*f=r99OS*GY&(vpH%p%RUFZW)vnn zg+Kz9Bt0pBx&SgY#MM7tB-{SA8yE|GSAeZgy86773)PwALTs2|d=1AxTOF)rII^|! zpz6aw+OaDstg~E1Xx1-pz-s?<@V($h3dXl?<@&g~QZ0az*L(7i8ZEXy8M+8xIC32p z(tB<%75DK+_94wU%HMwb+Bh*-AwZx)+}q!C!*j#KqY7{w(wkg^LhPJ)zIozlSw^^*e9+NDisri*^S?8hx zz2ntmUCdi9dw_w?0;IPjox~+gMRh8qe@xOUQ#}29^=l$B-;)bjfC!tV!pd)a>%4K< z=E@-z@TEYH@v7ryu4%#o@is6{l7>%u+k$V7fLL{OS8l8oXL@B~R~HB6yM2~gIvt21 zWGsJX(Pm}>X?MasKn9GrC3{Bxu630J47T3Ans#iMh5Ia#~y+?j@#q% zui7y=HqazbCfd#7@m?^HNWn9CN=(;8RXlp-z=XLqwYlOrzxXwh$h>?Xm^x~2y}zH5 zNOu+vYpOB%%R%)LfrHpZAV0S>w6wFbyrju)0FITOL!rsd<|#@Ka`y)3%v-F0e6cMz zz(hW7^^3%8J)yxSZw`P#*a_}|+MyA0j6ED(fDOdk?zU{!Y=o>{ZPuD-R2wm9M%6*L z>$w1+A1$ucHgG}u!?K|)}3nn<;otjxuu@E)sk$n0gxVZI|;yd zJ>~EtBxKBY5C#|#4|&a@tea5_K`%TV7<%wR2rGSI*FjfDBgUrT^nA+9cXqrI4}pO5 zff`%3CdME#tTG33aJeaox16j(DuILyFQW|Dp-_`|uIrI*=atK6a#w${E2jGFNe^Ir zojFu7GhWp&FK{F!N$Sl~hrh0q5zWa^ z^ct+OlKEK%g%~or7Lv0L!h(jj9HdG`u;z-x+<{xQJYZ?% ziRT$`6D+UaL5NkEsCujKicisvuyrO>8k7QKs^rf9vzZ6H>pj7XJ6<1!Y4+j7rv1*v zPop8jj}D3ASqO6rj2Hms1~fu-lTigOH9TKi4(0*H+BFqen;IY-Ul)osX4GFB2u1Gs z@5w7^u~km|;h1Cu&0in4C=YTMBH#4C{101)4h;>e-1B)PM2w$}11t2cryD)D8~2|9 z51};GpX=qh;uPmpm45AO8i(QuF-%LObAb1iGcCITBZ#c z13dCoe~m@XO8i@{gIvY72(}gQm}Tld!^n)5X&oIVzp8V@6qj?mu!0D{nR@lh zU+KGl5#HHffEVV$hi$uAYroIF66gHAL;pWGf6{j@?~s846B!j>v9_zx_*xx*(p=a|u+u$07Y5XD&N)0JTgLED_9>rWXxFv45Yhx1~k{JFU; zKl0q7lMN-+fVunwLbimzmgc1CxaL?lohoFDxbCg*^~+1wh3M?>6|emMrvCN#zZDuU zD8iZwDDrJu*W;b*T?CUae8oZb0f#`A;?jGAcP%Tz-CbdT{zk8})cuJOr8ka^d2@7v zBQue!;DiJ37NM)};li2h!9TgoBfAt@UixfgawU3eb!Xqu70^8_pFL{-{7NS%BsecK zd+zD^+$Wb_$=!NSuo$-wA##?bzdCWEr#4!CSB_xWk8=1YK`Ab~js*S5RpbJ6q%r@y z&PwR_4<3*cz0S%4YUhUij^M<29+MSMpSMroR^r%G;bd|dyFO390cLVX-c|;#2;GG7 z@hEvQfmwSV_%_H&Ts!?XR3z=kR{L2cs3`g3>{P&y*lrNx6Sy9yo5_02_Sokgc&#?{1l+Yc zl#wXOys^B)7Zjmg^jMZ-pvG$U+w-`(NCxqv%nyWsYud0U|1y8DtR+dezhvf_9Oo&( zOZbml1o(9x?U$b$gLZ_@O6a&3XEo1)l#?y_SL1OmRrV#!<$D%&9W2B)f?cZ@k{Vedma4rlKt8n-Qf(+^-prwV2ReAESLxQJ2idW zW34a;3K(W#84(xTO@0$U{8MqdqH!vME6YxnHa zlFFbVe#);(g!zw1*^|klm%CruW&-w~fI%}40F6v920TtOnCz8EWDGXWwMY& z$@ajUTHWCyG*?|m13S@Vb?N)HN9PO`k%Ajd698Dw4yxL+Z#hxlsYSMZVV#UPompdx zbp1)7P%&6wX}$%$DJ6XHChVhVu!*x%W3=nWM~N8ohNBF|ZMXYdFxlMv?%TBPxpurS z2MhQE3oVSuobW6wS)wwVy3rXc#}F@b;?2pNp#|b6Bz9-Yjlt!XmvP z-DjMzzET%Nex9xw^xgc9P8;@yb(SbWHd5CIzX=keNn*q}AS*VnYke&u^lMl;oxll0 zg(S&k)Ekv-odCXpGSe9c`RrBr#!w&?b}8WkPxl<2n?E-jd-eaPszeK2)oJ+LPy_717*JMNi7-@Z_trv&ocd z&!O?&Po(Fg{AO!l1u)~akBx*qrO9^|Qe`Fq(@EjQ`%K|7Laepq4Qi>`f`Y?r4bRxm z*e>1Q3QW+Rv4o%8?{a2(3dpm&>;mzsAa)Oz)ioP!()S{wr`7n9U9(#k z&Tmb&hPm)=wL1S2s}(*Hwly0my^;)Agd?elb{mgu;M*@;%Xh(_5#}$)wZo+3|$i_fkRv>;Z@v@d2<5h`Nof z1FB_*`mhY;`IF^SBwp(OX>xnEEjypf^T_aR9tq%F0o6&t;_+?f(Anvyk0%wF0DJg_ zBh+2V=c}!zgHjG`&r$$DSIf9YXyJt*n^Xd6O+8b`{yr!M@kA5NlAf^@HFU}|X}3d~ zC|z}kd&|!4x zfIn+pX3Ej%w)n-5kECB27ihv?qPK%Ay3xYanC?RNA~t|o;DnPNtVlF#&`gS`QXc}; zv~Zrg?F{R~o+*f=dpwS=yt3+9@;hD9V+m7E(-$#s%ANNDSGlPlmw$m*z^XiI!M%@Z zILab5l%jbAvgK7mImCS9)k22O?EzS#A5Wf)xzLpKGE<`zGSzNl z{nZ1xF%t@dMw>{YVd5M#Bu2+K1?P-e4(<)rw zgOpO7Omf%PwLbGPW33ydJ;t%}TqIk2ykp4HEQ6yQh#xa#@JB|v)mZb%E52wq71 zB{F6Es}%gp21DgDtgRWg9lESI{QK-Ft;+v`2?wyy66g+<=s=;n;9|b6t{Dynqp`0h z0!EK}`)v|H?Z^Y)IWV1;8O7l0IA^|zEfX!Xo0C5jKL3`W4Pi=pxge0!f!f@ijepUu zJ2``5=Sbgl`BKPJ0!vX3v!ncK_$De=t~r9RZKQX4EC4Z*zFJuP)jAY<_L&_y%T z2#yqUWUiz`U-Y{s`6S*H;#(IrC%PEI0zWvfS>Wvd_m$WUlU1_`A* z6>*E*GkdZW_`5wV34f)t(l8sJjpUK!x9R z`$@o6@Y0ILxuevD7BV!M{}G`236NXdgAUWj|K4wIyJ6 z#l`Y>dH8tkKEkD^XX^8W<;MM#jI!d&+Aor3Ru6VSv;NmW?J9y6>fv?Gd{Dg%96yHm zfX4W3anNBRuu9ZPL6!s7O8|D2rcTaLT;v9d+2yXyO;1tC8b?%yus66ACa;j=EIhPo zrQE|uPvZg8C?B$Dhkn^vvBBpy#!k5$Tq>pzNcJAR&! z*T0T%FB%}__JP$bJO(N2RK1AcRPm9bzDW0xqoY+nSBRCi6&$-d_sIT>W@H+W5U|We zfi=~;w$3t;w)@6m_ii-RWF3C~o7u3~BQ5PxBwV;r{>zJwWfJSfJ+Xvyp{+G_wL#2K zwIHL1eH`TS%DMORfuao&k#IJWwsB+&kRqzRd?md-yPXDJ&^~M zU4DAoNqw!zRk7{d`+NKJ_0dVtWM{_KBuI4HTDa%>R>Koe^-7+HD5=%bkb#K(t8WJ=KPkFRHH06=eE|@cz^xoYk0Bx&xZ>*=zO-r37qBTz^Ihx^R4BRo>M~8 zh?eOG5!wk8S`_kPygMO25~mBNmu&aK*Lo9l*{_13$>@9>uq5P&^#A^F`P+orI^y$g ze5K|teMOR@)WYz712-hG=8#p|5CHFu`sh*;hw3R}L)b(E0!t!9_w)0YsEt$IgXlX+ zq|@yfc#+KLgUBmGbHOizmpJtOup9M@DhHmA2Dl!&Z?e5xNAe*T}jt9q&SZg_f1=#VO$eRhU9E`d-#HxLTr26n1GB{wYFboA7``L z*^8V%F|Qo;dHj2IdTitULw;gc$|mA#o0Q>eg9RNFXBNSXKfv!7F!^lay|Rn})rBjr z4qs?k{E15p*d-YH+d^|e*&PKG7~KkoDN~ux@M7tc3c?e{`UTVu;&2|~=h^m_ugj?! zb%8i^F0_7mXoc8?-%+2PDe^w98@kOdoJipZ46fatX9<3T!YOP<`rg3Z%s{!IyP9Uk zX>v_bSK{4xPijJFbZ0&I;MSb*&wnp)C%@>UBYq1za(;O-2KUb zd&lC)54|Xnw##nic32YdtyOhAKeE2Gv@C?Brxc+dN`D2uZ_!(0OqJ9iJBqBd*YI}p&FCF?#q9#;ykN5&t4bHfb?dC zVy#Y$Y%M+QMW|H@?s=5xw>6AQ?oglK8{ZLEP!oB>fW~n8)xc>fRpZ8-lCX8kBv8Ky z)Y5WeQ8KcAsf6C{%pE0!*t!b!$w8jhK9myv%M9fXsm(Bq%eB3@wrXhW;z2|WPw2cv zXdkk=mU907lQ+nFZB`}E!F$YSXyUiUT)DXsy2;2DchXqLHES|CCZ4pyCro}49Bd62 zoeI?Z`QyP+3@ib05aCqhaotZ zrI9#h82wuEd6v#T%5LA$W21hQB`Juivk$SNlqmA7Xz;~g$RQN#gy6g1YDP^m8+^#D zr+A74E#IpiyW20uXxuPRD180$_nY6RKRJPt^RP1tm}TFq->&fN(`!`Ix~MU65hEq_Hu_l+8s`-qpY8K;Gwk zC=>FNpyBv^m|zK6Y>K&IWOR~PDkG+VBE(FP|4IQPDmSV@yz~0e79e=B#sE|CKkxSQ zM)e>WGZHT5Cx-sz7uGjCLS^$MFUYq{G1R(QnJk1}W9onN?t;C+eu(%8r2Hp=Ix1ZL zdep86?_C)n!7ek4F$>|_Fnv#$A+zWm_-(~qL(r6HJbHUmhcXl=%o2EF*(3g%aQr+F zzJ3719JHlBJJbo)tWxh}mlC3=kkJ!|_vfi{)tYrH0!UnswUl2|%G)AS9; z0hy8)6vk6W+~qSR=3&GwFayCZ8ka({k!Wo6`l+N#WaQ~|qHQ^a(@k-J#s3&bnstEa znmedmQW$@}z4JouJLKI~eW+g3bP>)X`K&$r!}-jiq-r6h9K*Gb^T>HSsS~_*f>EXH zUghKRS8S)4(ek@1;*rAN2|tW3pF2|X$(;?RZNA}AE*tk4=rJ_;`1e)NgGoDUq6$_u zk;%WB{58){m)7$4>4_brYEcg*?cZe%J|FM%br8L(Q(m?xr9fe@M7sh@{!x@WVsGUq zY?%$nY-04wooF>;EWPoSt60poCK#Q+cdg627Q+9=rb6O2oYBn-1(HzRcMvtH_ARb& zbY7fU2O7-IKoMf-3JdV0*OYuCy7k2{#8{=h|A{3DAuA!mtQ#N!EXj{Q@8o;}EO{E? zzh-GB5C#J}SvVSY_iIr*s!c7DXkXgDcn#NN3Uy}xGrzwJ@TS9PzTUze*O`HjC@3pQQ*Nk9M;v&SSe9d$%#HIZ5^l|Xpy z#p_^}H#pw~EitAN49G9GWQu2WLft-U-f`^5$+^QaJl(1ArmH>UCdU&;!Ax0*3c|LI z9}_z~wKpaF?7^3LAcCrtOXtZNI#~HoLUVXut!IC4H>6spcVLhH(yNrJ)wxw=ETeu@x@)8u+a=xd z;q6TeS#b}$n;h9!!@m*ij?$z;Kx?!BCJ<~o=oXuSY%6q@Yl%vFFf{XGWKf`nt#rV* zOZYfcVhv2T*??_p7{Z!v_XOHk+2@~Bx+UY+-A)b>@2JztV_AHpLR4M@J_ITu^R=Ok zNo;_zZQQX!URZo%ysiM12I{1#|FF$6b)q|4L0#rV)3sr%n&A^Gor*^y#NWJ+I;9Fm z<@{`+2^zmJ#R=!|T+>~HRZagS=&cOcGZ_wkkuAAm-Fq{WmD&;*Wg2IN0bwdoj(d@< z(&WVwE`4+YW7lC4SXBtE^Mg^|wKxNpQdZt)si#nYKw)&`A#5E93`R^$tn!PIv`c^UPcov~>Zh&A2$vV3PluTWL zgrwUi&hd@pZ55(lyHSFO_dLc0GH9z$6djb%kyj7CS_QDqN`Y&D58kytAgG^8?2~lQ zz3`~6If)baPE>3W4){JPzRdj1 z8u$xV_}zDVSXxu=q^Q+cogUeE?>O9=V>UVPYBoBNq4s5xRXjlM~o$-&d{7 zrN_@7Dbm^QB*~pc5NjuPgd&zfU_@DMB=ozpu&6C$SfwgO$xtz zmflg*S-<(%TgLr&BmvN!`y-Oz6sAM+CN?pUo7umEX-A& zy#8@45HDLT7%VAfgSeho)wiTx3L&q=*!|GrdW}*3puTq zui)YS2<_+brT4dbMYJdLrJmHc&x=6w>CLplRm>+^PAWXUZ)1~S>`f^FD?uts0m8|b zh_Zfxu(yBRvHw|FeD(yKy)IP~D;&zfdy6WZ_3DNijzfGgh(l?tx0Q9ujQXeN&96JX z?t7+YotpKm zfFXx>$w}1(@<4BDqdkNZ$c>?C!}Hr#HMw(re&sZ`ucVp@@W>tLFEJ)JeWkl}2W_(+ zH&%+R)_2Id<`&OpkvH){$f6l^@P!2a5;@Z6%jb472eUD}T^xDA&6a4hO=2IR-wXs) zAYPwH1>Y36ChcL^U?g2J>q-_D^vkI_p2~{pOPH&@T?Dji()QMY1|{FYbE7;i5$5O zB6SEBTa6k7mra1G*>8~ce{=??8wz?mjI3v9dtTsQZ7mhz$gr;ZtT5TNEhS2xF-?bz zMnt+`pS7k~+)~r4&Cm_auZDR;McjZ`C);3hlah(O6C{*z{T9soIdUj-`lI;1gu)8K zKvI$oFu0aRhQ|Q=^{?k^xxVY()tZn<(fvGlkBnr?DboQ5jvlNTOfRd58vOCZk1nn7 zo8g<$0C^qy$1mlYhLyD(_5^CQ=6>`$C^23f>xuHN6@{*Wqd3mjdJ6tF-AUdLP@Fk4 z-4R_%LsEv1Pu`VDd7D>~vYjf>_9j(9HXZy?3l;`E(JIz|FVaW>R6iDQ-gyyp*69Mg zu4fw-^oV9RH2~39y=~~(1s~eiT|3;*Q{$v8=8VBItLD9)rLVkykSebJM@lm?N2xGxu(bTlvjju`Z!vZ~TS zjr&UgkBcMYhXC}|q@8)Q_)M1{;m{7+w6|^4hgodM{7v;`v9lKC+oxv}=>I7;>!f=@ zY8VHeFCf(vMr3DcJv+!zXN&Ii=%Zq-B+M+e7h?s&;2kz48Xe1y-3QWy>GLRg+pCo> zE1h`Vn!J$UfB-ediACng*hbgg3ze!3O4X=ocdsOUQnaw8f6Z%Khtdz=DpADQ$mO0& zs}9D(P@1};%?fq1bPH?TPpS0C9OQJ2+Ky}Yba{wYsQ$NA1-C*KFy`uLA|EcR9U-$##^hCd_gi7+Rjea%Mvm1#S4%+a8 zA66^i&)E@!t;28Qf4DJ5V?X4b7AVRKP;?7Sc7*JJ(4cM_(nm{7ldx3KZbwB{P?uy;K^!k_wjIVK*{lW`t)jv(ZNb4E5wb%pEL_zhYo0#|aTmjlQXGD~uEaY6V>RZjJlU<lq%1?fA8)7O^{Y3)2%KG5xWLPX`nq$uW&VNQpNriVTrR|^MM%8;NoDZ z`huV#H?VA&Cj`DxUcZaO^l44Xo7m8|)CAKF zd^2!(dI1Z>AzSa-@WRu+03yz5TTA1>sKl=5%HTeCU#9h}k)!qkm=w;f z)?LMfPp23k7yEn(4wJwrde%m*!z@8tk+T!IEhOCut zNO~l%z&x=3PAvh^z8Deq2&kxk5`YVIv*cB^BW6X*Ro7d*Wp)hU%i9)iGELh;?W6GU!Vj9 zw7)(~Xd|y?*|@T-_m61j0Gh1tX$Ia4lebOn{C!Q~*fD8QBfB}A^vKkM@C4kOj>!!2 z9M^j}WB^#<)5){L_mi4V;mdpLO)BXg8WaA_&%OY4=tS<;SvkJ+*Ny#sgMlOIpN8UW ztAEMx{$8m9z5q9;v?Z;sQbs|E0-l6hUy=sOv1-6T(YQ83vjQ^J@Wo4XG6XV90tT55 zMhYsK-lsscz2TT}zSTfLoX^Bo3R}(V0jl7R=T{{_|2U0Q6LTB8N#1W+F@7EW}9bd0&@_ ztsSzi@Tzd2#AsGz(pPrYqu(=5-o4+`+Aht6f85u9^+yZzfN!#DZ(sP)ut&ZmL!z7~ z|Hx}}!KeAbJh0P)j>YHamR^y@*3}lT+39j?iLs&dnuYfc)51s`pJrlT+2a7OY<;DM z{sWc$skOF%{7qAnq5)v()RBIqC~0CWEVr)NOkwu<(^i--`s*4jb9m4~QYS!3QNFkm zefe{luz6K&`_&;fBCX)UkW38Y&iM*BndxT!Is-`kj?q5}Ox?yf1wF9oLQ)`w8E;7u7TR&P-h<7b z1cZf)U7X<&HmTsdtE+A*Ftt z)tdt|zhQ;TFFCDOJX8*GU@~p6ACB5BIVy>6atzid3F09=@ZDeCLj;Uj@KvtnHjgJ5 zckCz#gpFaH=0%f|AC-*^m@n#oilJ-u2=UxTWe!%=s$FZ(>uHU?=%jGjJu2ee9KK@_ zmQ=bs<={zyK_<`r&~mYJc$4%-zY|sgw3j@DQK5bfd`By%ZK=|vUS?5wVy^K@IO*_l zqmRqa0Vfh4DfmT@wOXVn%LKeOLsl%@>y#tAddU3==Gw~>`I`6I_rJ$p2I4s%Z;6J^ znswGl%z4N)IX&T=pSngTEo>W#CmbCRpIfWbkQ&@zKaylOi(|)ZJ34Y;IGeC}oYMW- z+0c1~aL@hxm1B0$XWN(gkAoH?ejkwd&zp5-rLKi-HLX2b zWj##FJ(#)Bfuk}0Nl-=$OQ&@wW%c0meDdo(wP{Uj9vOWGgPCYVG;eL>)F-jm=NczN zVCPM@^5<%=Y23rl6D?VI9pfHG<3aqQJX+?^@*PN;?=)C%ndi9HG7@~De_}2@?4-*3`|($P zT6dmLr&Ee;$QuAzA~$D^U)Vw?Mg$3CFJ6f2+cij#6R>9sBJfo7K;5Jcl@`Viadd#sYXP;eup zJZDB-8FzoF`B1zo5N~+u9x5}mSDhP53Ntdrs}y`|-Uw1y+Hj$VW%an6wl(!wvL))P z8$4A&?h>?bSeqPxZ|wV7-80J!P!}ZSe*XGqEhc1t%Ir9CPZ)F%6}i||`1})36frfu z6wxYbH4f}SKDcaluzz1q!2z4v8~;7&4*i3Hx(HS*5?GPy=t{z9gUyBM<}4O$+V`1Y zvORkKG!-XEq%@x3Cjm~?DTxyEz!e`=a8giTl zwe+ebC+9n|wj1WNNx;Z;y)anqL37v$#5!f{N(YqEYwppNW_+1==)dR4N+Q~}qz(Jz z4!&Gc6^u;xV2%pRY%ZRbp_=TLX=D$H_h&k9IupeA@PZVI@9yivCYj*}cWOTOcD_?} zGT);jG^VrbAuw<**8_S}sV$UMePe2{jtgx=kWgUyImf0D?i$U3mP*eDJ=M-cPs0((hNlDCH zyR1rvwpd8)eiUsg|0Fp77?f|YM7}LEMz0j8s4|b3{vl7k;%e3CcJysLcu!#cwEaU^ z{Z~-FeW5c)BLhQ_d={Uj^xf@u1fUc&>7rSji#JuZG;~qr5r2B_fJ>67nW2LRSEPvr z-ww=?=>bf3tz^b0=}uh}gig&SX;(}Ov=xM}K=iK93;LAA)C3DqBydTV`$<#b6aZi7 zsNp(xGeHfl`>%^7`WL!wut{LqP>aA^v_zy!^v1VgywEO~G4&f3nl{cApRFrzR~~hN zjCElcpMprVYG9zCwUEz@t9d&jf)Z;vI+b@J#}{Us23H*)yM@?ZsJ!DrD6V3B94QNL z&6)`(MLYvFeALa%G}WURU9#`tBbDLQ0%O;H5(L}QLNHX;DWhg9fP*HIZffji#2IxZ;KP3?K>p1};>#Hi;9+Su z(usR=LUur&)>2yeBeA3;v7XeG(SrgK=THcyE|euUy9G3U@KzmiUi@_bCxHmfE{wui zVZBLkLSfZBjkN<#4;vHnJwnpw4Y^z9!B44yp-g*mV}~M20V^WVIjw^0GaK$5CU?bNV%J1Tlgax~ zsaINUlmZ8mer+V=UIBKW9amq;eipwIB7XbJE5dB-lNOBG4VZyxi{>Jt*=bey7<5pd zXeY*vc8{ZL-RaTTI|M6}iH4RZiWx7fv43B@VJ%)@fwovemVf0s=&353D}3p7 z&wif~_oFJ6U)2`k88n@sqw}z^70cByW5T@_mjJ5-@jdzmIGJn$MDdx`5qFaX5V(5~ z@LaMN)A*qb2+_tErZek@5X;SGQ_>1U;j~np`tB`oodXpR_-jFDAKBrBO{Ss1}G<>CK_(!enmc@ zW(EINvqE(+q#8FgTT&-J|0z|~YYrWnuH+`}l?`97YzeSTizM2g>7UzAQ~+3HNe3@O zx$7kHYaw1cIlwkUhAA(kJ)PVDSmu6NApUxy#JK8UQ&x*6Uw&iK|Nl}k|FiY)FLvoa z^hg9}Fq~=>tMQ@)Ai%nFLbJz3;{H_Q{MfqlMO?}v3F*nNc#iento$rgBCB&Tdx^fH zrgVn0?{`PkidBg+o$aHkMX!@e(cuHUp6qU5yvfEE!?lbc4U(s&Nuu5q@yU z$e&Z&(DELL11`-_pLNO+@;IQvsl#u?9`?gQ)?0Wz&L^~RzC_KZFvXOHe6FnieJfBs zOsTfzB<;g>zZj$;l6)_w%x?e~amFvcS*G;gJsO=d2A(ohr%jIo^)Qc?&O+bSo3Dj) z9c=)v54WyX_Uh28a1as-IPsVp6_Yf((G-6*1Jca%oNSgNwmRW63Jb>pdt4O8->}io z1C@s`OqjMhp8B1EGd~5lMW-yR#)jN!l`jvT2~2(p(@atZDCkL#|2}S@DL62)`CvO$ z``d%}B~Pjg9B{&T*1?*!%%bYJ#sk_4TJyq3hT!E=7DQmCn2h!aM$OM&-f%$@{4V>ajkAXw)A#X!{+oX$scz7Wia&U0)>KuB8r^j8(^swLi1bmPu%!S^F?|NK)rjiQ8(4^B zeMHk?Ty&tl@m`Oh?8m`{_KIDm*whE=QmI*Y(5@$H>S&`6d{U)@?sEBSWb1aV!CYg* zE^6JgH)z>Cq!c``S(Z5NIX&M)Thd#7oW)r_4=*R0JF;{q=5siZoJphMi zAAD_9SS)a^_6f9NkY#wwf#adfwEh#OR}ZXZuj_F+y^?~wT(r-FT+uV(!6^=KmYgtV zX!w7rQJQr5Kt#yu(gxBbm|H1O!idy?D+c!2LaHxkr+#dlGe@MvcRq8Tru@tf@{M*v zPG7}bHF2%Yi1j=anAS!KULpEERUNgEPT-PAj>N7wmuQaxLtoJrC94SN?{pXhm4m(* z&K{g{+-e#(M!HL9m;|+`ZLK}l@O?%7o^|Kz^>Gd)6DBAZLx+8>r3w7%qw@N(OobiF zcRkY@<_k}Xca4N{gf9nM&E-qCj>k9AcVV3#jbHh~1~W-lCf9US zh&yRPM?R0XspgA-HRN`es)u)3)y=PlHtmPmocmh!=dip@g)8P!<38EfbfGaW50EW#B1QUpeQu{*G zCqu4kJR<q#agXn^z2%|NN6h(d#$YVX`A4(G+a22(}LYB&43-xY$2-H)o z1Bt&I{pQs2xaEzTS^nP2Lqei3+0IIAa*in_n@O4px^d^*a}${`fAD8ezNK@7CR`Fd z7j72ua6QW(L||zvWKMEf2pp-xz;0VB$2?lmMtpnHa&vfin7`nUD}M&Nj}T$J9) zX_{XIV3f>Ly*YdWF(?wBY?ofQEPpXR-Slk7OH9(?R7EZdHc7FSc z{M-p#m|)OkN3i`=7PM8qC%PUNBJBf|Oa@^@hCs;#&}xht@vNm7NdPU)kwQrdRDC-# z(o@{t#D-dqcBpBG&!mbh4v!=)hb!u^28ytOeG$0!Lzy>sPbU+H0TKql5Kgpn=(}H0 z%`1?1Ru5$VQ+n<1Mftbid=lIiatO=H_($}r76bJFNC%lx+OPv%j`cMF4Fhqw47Iu6 zlRz{ApP3W6?F5*jMWqj+XuJVRfg-ddsrq5}n#?NIRuA}wsg1K)lT!Bw)v)u=y^{4C zLo@ftXTx9Yv=6>$N%ViGKO8Y#ZRM{yS2r@G;9pE$zy(m#@fSjyQKS%ZXp-ZG`Cx#e z7Er)&eEE#X{jSS^pF$1%csVvcuTk_^9}wli`)QUU{}E@ZJC!MJH4C~Rf;x9brdEX4 zbJ**Bo%OtkVh||KBvHpH4bVHw%G&*iZlc%dbyvoa95MFi&v-YNKNbZFJirxS*GO4s zO5V+hf21io&WLlvE#eij!j(Ok|DvI8ZmwdF_iaV381LIqiP=rc6MZcRx1EKRKXaeN zO1=N-x$J~5Zx=ffhVLyN(-(Me+(o0j7wcaT0 zr2ckt;DJ-lfVawb_y{_G7cc*f<9`~@lUY(z@vTxWP$P*tB9$%cen!0JSJPwOVLnni zjkSSm#D@0*jA)%6oetNviIT43a(w;8K;$z{DIdE=^8FWqJeyNe0AZ~;t|}nLPVY*M zSvXYvXt%x%PvXXR>>Ys9og{8u%?+=gw(0zoUFQn6>8koiv<-iHinEs@`gCUe0V@Z* zp4K)*lnAhqA9Nt5mBgg`)s#f4xxa=`9evod0W@K8X2RrnU%SO=NpG;N>Jn%LmZ`bi zJV&Qix2I8hT{dD!h7nm(KxQl^{7u+nkEooWs?t(=ILX0P??gH91B_q-Tl2x?U%865&)vpRLZ3Ze<)UBp=ejgm(f@ z#kOBoyJ?LwTp7xFd@7xFh^PU?d#LJq{ir$yd);a$4xoS9|N4)$0ehhtre8@TKZKuu zciMI9knZ3Z1O&(3dw=Haz{YQH;ak8?PW~bh9x5Vq3U@ipG>s{(u$sP4$M}=r1K#BT z!BcD0BwWJ4(a$`ze_gbZrQ^fe`dC~2eu3a$<__1otwQJ_5_q@4z?I%^2clKKbaWT` zoddx1a7q4?dvp#s66(*=+ihrm7&3AfxAlApg!CrXi@_sVL+!!e))*B(a9i1Xy4!0! z!F#}j7s=0z+;0F>9^Kk>3rzGU0Ub6qYkemUoY{cy3XJ>Jo|CW%eusqKl|fqY_y{}= zEOeuSM3+z1A2%O-tVK?9{UyRy~<$_1tn*!d6S5odRkgPI+)Y3db2^ zvQXBI^n*=Fh3Zzja?vNKP{5Z{c;7oII$v93^*G#At#EV9F>K?ep}2^_!o@IDsS&X3 zyE-*hb5W0Fj`O)B=?KgZt~M*a48Id2aVJQ`0GD6Gs@O-LQy$ExoaY=Z^R3tm5~vr~ z=9o%c;}>?Nbs=ck*p`q{)Mj0qGcijpW3mRF!z*&@sl-RAF@57vwCh~2CvR$`DQGUgOAW~EiqJs1yRjCmXLJ^QIT}8U|USp*f>Cys72|dy~sPx`JdhZfC zgb?C=@n7n{*LJV9&KY~1ea5}lF)|Ea+LteHd7fv^XHJLPM8YyeS9W&!UjZj2Jj?4v zD9LuhIsH!JnDX&v$934%x5Et@$1Vjtm)ZZoMt7Db;e)tzw^b{Lz_UmJD zH?KZ=OL{-pb)I!RIb+#xsbjzoyE9;clS{{p+;>SXhLahYOYojhVU^ZBANG;yHjb?wxLw@~ zmMdSs^Kc*vFChCS~i=Nr+bHnry|!!b_YmqgRGqx~X+h#9VrW zTDOGZm6P|wx*4^Io!*4bXiEu=L_4qWvBJ2gh=_KV zo^J%50BWiZv@nBi9@EEyylr{$7#`1s+lC)3cK>`BD0brQ zYqq+92yU`o-n!p3Q*Y7j5~D@gfFY6aMo2DLPGn2cmZQo=m&YIKh>)X>MU`&TCqc9p z>XjTaZZleWWvBP4m|)#j&q$w184Pf)nWjP=mbn?H=`v7-NE>MQo}6za3PbEK`L*?V z(qn?Kb=tA-CN;@Px?n}Ip+dVjZk;#QlbdTsqnSf^CQB~J>V-hz_w60YZZ1sGb}lx& z-w0e~&O!!~wo-wEVlbeRP#Y>5n{pg}75Knu81XqBJD3?NC(+hlP>gX)mEr4IE{J5I zGk8tjUy<<91{2e`5m|~w6mQhB9Pk$#$RoX=b#Q3cnXg4FrIjpGd<8`o`T6@d5&L*1 zyz#Q6i{yO=r(0QQNfmwWpVtNUx2x96oF&+}|MQB|{kU#NdgDJY=x?w0;N!{8Gm1H~ zC4=qPmlmCAe0a&exVa|OE|I>fgfN!ftgKX*7bWd#fjwSbhLa-YXCN2rlRq7Xq)=)< zub}NsIlBlQ#;bKJ#K;XR8c5gQXluZ9Hpr#j8&qA8WsVTM>D#(1}W*=k0@~ z{we28AhHBsNCv`GQo%y{`-?#Ay&LEfll~eHKOc{lB!G5A;a#DJ+a|vrHlzSx-;Y;j zQ2}@O1Q*KwoVJkm%Pk+QwnA3bn2+)d(?WExRkuUVt;S)BNDZG0vs`OQL?;xDC6;jI z5IEJDkh(y;o8MN1&^xV!3cybIvb~fVq!T=&>xOWcA|=)_S2v$&FMUS}vZZ4iW?D zncn?2%^!SZC)jvjPu0W8$weg$Vr^T0a9X63MUbbA=`dO5=OArilW6=kWHGtus}VQ+ zTrS+Z77^idwU6%s5y}~GoT}M+6Xg3M#0Cp4T$_}I0;1Hu1KlW>U!|bS)JU5z!;#EY zhOIN8AbrmU))hH&>b_I1EXs62ShM0uw{apkz&?X-OqTDK_7P9&Dt;48Vzyi!wJ7!y zetCIKv0n|VXU9F$w#ZNjAYxTFPV5hyvu)wzY-`Ul`TZz<>;3N;o2Nrf z9c2zc@9wzR`{)2&ssx|;o4pE_0^MG`|(B(;U49k-B<3H zIFYw)HuDw+)kpg~Fy-+6JX4Kuv)@e@c~f)#ol$|}MLpBF>&$5Rb0g}f^(9I%#!1Z? zX6>0eR00FWvwy7j?BdD~ZR9ag(^5744N(>>2cpsYEeTaz06`t(A6nL+!XH}~61P+x zJ+V0rorowenQF!npva7j9gj5SfLAa7{8ZEco6wr!dr@MEU5W_GivFNq)Ald3+7*WQ z-HB2VY_O+HO7CN1b^;kG)}vnQ&95<9R6k2Z$RT`j-U~WbFYNVKMW5=*9x33(Z5!js z4&kJ(RU4#Q2a?OC4@%IJ`>Bmi^e-@^3#&R23578!#mm;ykfQYToDySt2M;?J2adL0 z!BBPfMy8B(_a8rNw4_50$!yO1Uo+^pi~j%0!uR{V1d|r!0y?S(@brsj)IWT-x{m!E zhySLJ{r|$-o=%F<7XmrKIHbXnzoV`s702M+gUb( z3Hx-h-18uZQ^u0Rn}bRQfIidSE4UT^U$bOYZdSuCBfrCAW|C~K*v=;eM zr%e$zO7LC1H~fTsPflI84TV<{NvsVH@>hOX&n{cE`qBW51vUpp%4&fE2{qE&xa~r8 z&bztQ*(YjMjY)fu9M^Er_>6gz`Y!gm{biQ;g1a8#XejrJ9&h?mRE*k)w%*5XX(8ie z(g$vSUc<|7!mmq2Wgsf#ftqw>x|z;2o%)9=*u6_Z~~jD#l93s(Oifl4Sz7aerbJrZb1yorNAW4gGQrT$E^lfYYY1Ca*E!U^G0vR4|br;I; z$(Pbsdw$I>C;jv8g0n3Sv8sn7aXQSjGzFfbhxnpTDhi8pbzKo>g5rL^t3^Nm70>+B zVfdFgX*c+k&OQ9a-eLL(vQ@ZIry$O}o=pX|L6VwOK*O?|99Q`47MmwzO67*Vh17c_ zTfxwd=A^v9rY7daV~c$>w;t)wU#cJnldLx2ByM;6JR3YS&@rPCmGij!(iey2%wX}~ z1kRZ^|BK80&f-w%x)|0j^z|GsE6TbCH^;>3!BOVq$^kgEhA z`APWnW*hbuonoJaR7QM$wJ4nM(M~UF+JCX~)|B&C4w7aDMMka%rTeifhw}FmBwV>= zx7_G`HyjHN^KzQHi!BllNA+HllDsX!6Z& z^CW<oSJ;Tcz^qgrp&ir0K&F@OJ&AUhZ}p{Y^beFs*@>g{Fj2f zR&GHCY_IwEbb>{u8I&ylY`{J$BmMUF_bSnU&jYZhySQG5zgjUmlAIra-__UO1v$)v zu|O8(2+eeH-F(F}&RV*jEA8QYK!|!A>I5Cr-&oa+@)c1d^5U53Elds2WqnRToXv(7 zRYXpE4AxcC7p+$#VvOGTa27u%b2*R70ECmB1UPG?MvO+JhH*%T&7aF<#{!&ubV@Il1(HOA1nftDYt!1xefB)et!I2E?07fac1XEE zv)3ar&5Kxwl4{|Y?3#3q-)@V_mF~EgLS&<4qtv@{XAkM8nYcM8zxX?LEQ}@Ecpx*TpOiFTXVBD3M4-7NUp#omf5Jw43D&@ z`d|7nr+_;8Pn_r2PkR2(zR^E*8L9}UI$q7oV%wHB)09W)7BI2vOZ!-j%PC6*X4n*} zn;uKrCu+*8Iwyez?ds!vuL;Fs-Mo9x4Z2~dGH3$-#EX=;s_bI@=vJSwFxAJEHP;+G zWI1T0N~@zT`SM&mXmcjq`iT0Y6d{)tZOQlq$R>>i+bYOA!O8hiLTK9SRQuNsXTPB9 zK!n^mqjSR0KdV#NQJ7jumTU-cR&-L{xEg=%B()5ccbvMdQ*RM6fr-U>I(%Hj>R<g+-bQRJz>FzcSSsn8jxY!<_`^1|FR39dM4+(PB{n(1*`4!HP4PA*~T$X9kz3a!E zb(N(lkUsNzq|sFZp)h%%^MLo?9g``#9CdRzAxw03b)N*z>KFFFKa4j^N##|T&^vX= z3C!(+H@ul>tKB)v331#T7m@sCkxssBE!f{=n^BF!gCjdikIx#Ny(?jmEHkkQ{B-Cw zA1C&?%vId{tYRjzIj#k!vM$lXADGRL|Nnd0`=s87?(j9zh5N`_d^L2>d+RS)(2okR zRt%R9*EUTsPmlgs*w*LK6@sV& zgoU(DgKQdlVtnyU?$t3n!@)Mfd2xIfzDg{j9Y$Y!y_q~;C@zcT6VaE!Bxk$nQ;&*c z#-ZgUS3z*%4p#zAAGveth-rtb=R?MA(mK@4I(pFqu=kth za$XIHZ3{cVknM7mDOE#w;*D~)ZV|Qh9&^#Xm`kRU%L9#30Eep-Ek88x{9#R&yUahO zCDLU~tHqnjshI?z+b>N!TqjoYR$^3Rq>_9YgG5uHxT0X>yb$Dbss05MQQ8<`{Dmxdl*D9&9*dxDgBpiDHG0@9$0U|32XBj>`HfmUGUb*rrGr0tj>?QB0UrVdB;7&ZN;%Uhc|pD^ z4*HAYH+?F7G!7akz7Y&~U|E#a7ZCLgqI$LND%1W}#1C^LQrXSz$;~o@BTwvzfu=7p zydpy|JCM=p09}#X%?x{d`K=6FBKZ{}L4W(Zr6Wf}Y5$D!Jb)Vuek!^iu@!%JW0^UW z05w1$D%yzk0{IM_{tof4J!W`)!Q0YZ++!|HdF&Lgd=P~zsj`V>eJ_$V^ufyt103&E zR`B`pNh4wD&NaivMX)?XI3cbMe#PBLgCR}36U&BtX0G@&9_i!t7FC(mek#A`NJWMm zmc=h~mVP6!sOjXNyLFLVcT~p4h8|c$*a_3gq+^PVvy`nr)(-U!B#-T0Fa^#4^D$DP z8aJZf&4wv;OKQ%Pcsx{V=Tfcc-Z_POmdf3J8F$I)52uqE-7+ct?|Mzg=q8L^Qrsoi zS3U{-5f59V@9aFgYbch0Pu7#0CM6wrc2YK%D{2y{XFdu7py{h>+d1lMl6~}5`Qa%| z@RPKkXg{G0ip#q3^v*W#BktY@y75~d58t^4FZ5Ivr@o9BD4dvdWof)jBML%`+Ql4s zH}T5#-IM_*|FtX6-SfH90r$T@N-yqdN?CH=f-EOzCm=7dzNUNvs1gd5LFfVXAd?O? z@BJmU#U$ZlUtRT(Y0hgBp*?7R?KhArAG!y-_D5&CHt%_D{czB`h|ucx82Qusn^$h* zz5CQ?&K|_`>gP_Xhsu0YW=khw#e)Mw9%WEBQx|;hM(pFLT zyEz^F>si~XEo(m!V#rsTa59;D+5G3UCM11GuU}=VXuR_r=ZgvH?Q$Dr+qk7Q!7e1i z@%f1+K+@?q7d9xD`$LlzLq(yy>0;K?&2DTwQCRaGRTI^oyTVTLv70P9_hJ3?3g_*)S&bvEQ>CMZIpG%6z@C!IwN zd@|xV>!1_iJ67|auP8f@B-~b_`Awr{{aHxv=#BZnfhQ}6u1y27jdPe+{%#G+r zIH3aCjP)T0wfnt4qfvritH$?Zgl@HXwwf-6Q-KRwl0OGZr^wEZ=Br%7o2 z#%rKDvF`^gI0n$u4Rf3vM$T!#%lkHeKa66sj1fiGD5@hAHXjtqUnxGNeR%wC4=UD7 zfy(}h2We8a5XcrtG0VNG0T0dDuGp52RC%LkcLf*WvZ>Qh?@`flKOUi?JSg@U-uoLt zj+W}@jK<*yPP*mquld-xM-+NsDRFEY)N8Us#+g>-`B&H$c%-1akH5mP{a?)WD7xKE z4G&Y(6ffwMwv65r(+~?1g)3n-{F`a&>0#G-gyMKUd?FfEkMx~B2dmwKn|z4N^~71U z+}@_g?x4;TlaB9(>$BIKbfd`gTr0>fdk@PtiSc^lY+*IYU9(=IX0wc}G9D~8)w|y1 z?(oR|V3EvHx>Z}&rE@Q&rZ#2@r?$SkcW=V!aW(p*tu@pIU==1L$)ld(i!)qY#+tUR z>rZlC7kZ>JwUhhj1|R9BQGM4rf;J$9qsfm##JsZ7Kc=3DQM zBeF1XcGdb3Dc4KGN?9lTQVS-U!EZl+gdP!sf5x#toeVK(d7Un!SAPw*%V62;HLf%cAyUf?<~^3j$%?cnyDGVO!` z2afxwU3WW|NxEltfL~acp^3Eh1D^7aS-jd9W=0iw&0O!vu?`mcI$ZPV{LIeMexF=k zx8x1i;G|mTU=0g%t(X<27NVzjcE7JD18Nj%?*v9Fz~=noggqVAdR3zjN^p!e+<33R z;2Qxg2oUbCH+osROS%==(WV&h^%@{Q?C>vY;j0WL#~4M06#_x$3fVouQ_$*(g_!XO zhCz{CPVVJh@;cA!`RiWMbkS+BOXc79UGKfvK2W{d)Le+1cZxBkmeGkcHVBo!_-n%r zEE@}w;ir|?%C%}zjsktmlp)vtm2mC{p^)c?9RY#i_n$!>fGN7>gVV#K7UFIBA)mQ0 z*fVEd-wPep77B!W< zeacosE7<9a#n|ZHpt8cBFWNcPR7ro-7 zEbNyod6e%23dDD#&$lPzq}HcoPBw?FacPvmm6l0YaFzbLT__P~hG@j}qabDRJ+N;o zEn@5Ey}!TB(8ZTXbom~5pIr(M2v~m3d>G&LVf31!nqI+ z7)Dh{FT){uqS3_|eAf~q5Z2VYwka>^LD%rO{HI0NoMhCZ&CA)Iay1|bbjGj?d z_bN!%U37glG?sR1?uy5tzLMrnORqv(6WCque4i)(S)=N^OX_fy)4hUMpT@~Um%l!- zt$sG1TJr{q?2DPLJeCMnu;m1+cwP$=@ym-nju3DCmU3NLMT|_PzEv`8>)}I)(AQlf z+KX6Erk<)R?4og^S!0VqLM1s$N1lPAU8{o@x7JxxSz0 zwN_Y?b`9Z$(MbT0AMyLPY-}Ae+4(&5kvmvAFG|)24s(k^M4J7^K$IZKxSl!rUtkxbEzOZA@rG)LIvvpO|npCLEKXF)jH`$5Y zII3$36^X`O2{7+hO`J7f8MMYIezqCb5@0mkd@Q&8uPku|pU7sH@_$!w@I6?_Mb;wtE4v-FN6}^ZSDrD=2f*{; zz-1~DbV$mmMDgkR#L@Gc(hs5v1IZhTucHUl$Y+i_H?|?W8-eC9if_Q4=J3oyOQruT z(`(TMP3UBs7lC`zHcy9D~EGb@cXeBk&rbYk*d@= zkdKp@r17WS(|X|MZ<#}?p@`h`UeQsM-Qsp;IBdznZi*5Ma5PS&bUxaEJm))fC0*qp zk$#L2ulxgA9y<}_J!*qnY2n&FsNz{&wR+^tq>w(dLzg^WO9jY$SCWIrnxwh1N4PhQsWiIs7RHWj)TcuAV&AXexb+iUC)0O33ZzSGa7( zl!wRmIW;4OSks$fiRkL&QFw5UQ-e(wor$p5g{)fO5X$AK1>NFV<6!(y896X@Mjm)1 zJ4<*~YQ8vE4%la621&XSDVr6G4@$;{k{d|Hqt@ zz;64OCW`-jpBz-C-kt%~sX=_}L$!+k5HkEso7rGe-B$dqodueS48#?ApA&Z&3$l8| zF3XU9DLU04);-U5E91Tmc0N2lB$b(^ozVhg{8{3TpA5nfF*Rs8r*lGQfp7zVx3Z>= zI#qbb{<+>4?!D85VFYw(8dRbPyiw}_y_G}{KTpfveOQ=ciK z9pJOW^ss+>jc_qDu6dn+?Le37w0qDhr*TR}VEMnRgTkQ>dsw;L%{~)zVB6G_u?|@n_r+@!5|#+xMSc z;-8NL4$FgyHB!*C_&3a=i;(#}YKdBMpvW^a(l=71AfeoU*@Ttb){I-@nWO*vG4t}5 z!y{a@Pu~VdT6wZ0F+!1Hqun#djo5H=VZ8^B@o}XhdN54 zPoebeL8Sp>%@ZHSB++Bopu@EONSXe=xJF^T7N})h*lgb|Usv6>86{g%HA#zASR^(4 zM&PSaM+&Iiwoh?-MELKY5^jc-9QRKbji0vo^AzhKvW|h-V~egY^|Zt;Suc97ppsB( zk)mvkcnFF2)7GvXXGj+|cmY>lm|Ig>L#`j_g5a0b?dPk=p~K4wH|e7QiAz!_{{$)=atceLl(g@>nTvSIiVc%jEMo#SJB>t~VAltTR;j*fNhe-pqIWAN zUD?6teL$9~>f~&D#ZuzB;%h4AGtr;F1_#%$EKevP*N=yN7BHP!JJX&yae=W^#g6yZ z_((uHb`M)**VE)~HS;c~FS6I-G*ef)NCJlJBlZc({rvf3V&Zp^kA#^IlD8Jt zDaVvED?%{_@99XwkVJx(onpW>wr_{cf2ebf(YDUjmGq;Hn#udh;s z5fCx-XgI8Q0$F&iPkjsbM>Nl?(0fId%nfd4XQ`?)_F;W z?Hnv^2p1}gxc|sOm2e)aH=&{?1~FMNeax9>d%Q3)+pxxqyYIWlWohRH^K&^XD!Nbi z>1SWl{i#4S1Fz1p48e6=_QY3Jx50TCf%8J`Z;J~@F~$F)Ddm4-f#GMtAvqP0*)67O zAUY4f7WK>NXxnR+mwSMsrhbmSR_7bO8uAgiiD{T5ZEb+B!9gfw*Be!%uQM1nfbd66 zENX~Z#MNAbipaM>``}uuseY9n@rKTL=D7^-K}1-=)xN@tcE+zGe<5JJ{CfgMfkDJmD(x>`X z%ppRB0Po&ix}-+3cV`21N)PS6B#L4xAORqUk~4BD#V$+V8E%$G6PN*I`%{F7t(7fn zy(FlJl$2+rH8zW6J#P*=Dq0;c7H?jg@j9Nx?LKcwl$6`~x`q@F#5MiSjG{?hTW!se zTI^frS!MT-tDjWHNZ{O<19eDXrQl&;`eE7_x1qI+j=dBx(Sh$|^>DKDkI` zoM~wF!D~LYOuV^I6=x?dg?$QYmXKKim_w823(45lwJL0cVf>BY%^!zc&vFw|)W zlfG(KRbRvcF7}kYT9u+gf=r)LewmKUM-&{meXJ31J$2pQnlD~ch~NhYsU)4T#gEC9d#ueRWoD$V>PdL!g)cb zqVOT{(s_sdVTR%MW?DB-$~Kh=Gz`DC!`C9ipqesQab|FR&l_DSk$%?s+Ent$%t zv56+PJg@ehDEct`D%y~KF3`iX`ehaLK}`?tL}F@D{>k_kn=?%FAr%!6MX;6T`l*&x zEj{zTG@)xvFZXF(N-jn?Dr8mEmRdh4!j+tT@DV^25p{<>?H|gVM zfPZgl*=$Qft7Gs8tytu0LJdLy;sk^`j?M}H+%rM1iZRJ0)s$guT(qj9^$pL>V+piG z8EZPL=&Y%8vPvAYyS%qAofMBF71vR!&CGYtmu0I_8H+HPD+~*_QkP4stQp)%nu{sU za>$;j8XG}7PN~|9e<3vk?EQ+5-uM?a5j5_n;ETX_7S>l&9em;8Ambc!MM3i->fvVP zDYvDXJpk+I1s7mI4Ua1J8vz`O-S&_HC7FZzof`D_=N)jpAl)#n*#j~jDAnNud{(};p78e@bOxvHJFRw5-9ClhVvZM zu9)v@&TeaD!=GX(fQYrcFwmlcAFx96+|R&+!ASYo3xvVC|MuJvyt<1h!h>o|Bi>T?M*6M=y4nt*uCyey+K|WU!N)nK6^*5vF`3bZy`gg!e79$kFWSeq`%WD}FV-?N&!WXv;ItK3$OE|6#+bHD zWjFt2c9g*otG(zu$472{J5@CDU3_evMW%bFX+ny;hx4;VPcfF$%H8X#)PIU_ZR8HX z<%YU9dGL@bnr$pU&Ya8F>DZpoYs@;cy|Lk5uBz|5l|#8wLPxkhmfgS-k!0N3iQ$AAK6*=^fUsm-mg})GaSX7#w#h8Gjyn4iU_C z2vj=pompdNR|;{ROgS9n+MpV9qN2#kl%5GxW6rtW!d<1K6{WK}z(YR=V>HGgqI5HO zP{v+Q`#>31ZbBsxgVrf4?x}<)vgh5Di9(BsIqR1vSS<`x#wzz(7RD~IuhFxxLOur7 z(M?yurPiwxlJsc&FQdgDdrO5=+Mv0$VmiI}YA%diEmxnoyw4!vrLUAAJCJ+9E^vH7 zUPKb?G$f1b)NBL#Q)zwys?Dyaz7eGP11qZTi?zDvTcRV^12&2MU#;$$L%w3iVZ040 z3LOgw6Y3klM4@=So+*5mD4b>Z)m4P6G36p1Z~v1n5hzuJq0bzU)jo?O3p$N+_4JGw z{bY3_?kojaw$0VgnE?cBL7-{4S9h?m;_WHodQO*b1d-;QQ={;YZyZI~YP+N8E2G=b zOgzk?7zuyxITq@aqj`BWwsf~nOpF$j;ap;dbJLq zSIc}vnbjaBKGL0h_=wf=_`XdXJUhC)q|4Ux`I&h<6~?VC5|C2FRNEYfXcaeZP4#Yw zMRZ~g>YyX#2e7mA9lhL=uWBLFR(1I28qwN^$nzOoUP3i1*kxCWnxRD@6tK3Oiywv= zJF>Ug$_O&_n!K;#?fjlbba!oM@W<<&WoS|t{8JyY)3pQjU|lSVE+nZnb8L#veq0kE zW=%NYk{;-a=(wo;?upp}tCRLNd81agtGYi`nCWYmUZZ~eDQ^f$v7_=cFyK{|H`YMx zY!b#@Pnc5bQum->Ik>Hz{&>la)l?Jo%gLL}+H1dsZgU^Pz#3Y)={FGQ(&OaMnh? z6eOr)`31RW>lR=SMfOw*-Br1$hj0y2YgtALd%d}pQc6l?v%^Ws2P#uf6y z-9kvGF=aXI@)LOEX&qdX8;j_tRlH38p|RhF+%-nmSWT6rrBa9~_5{1_Qrc za|wwq^v$vP}os03Ci zyoa1~HmJ2Zy{5%v)8tl61eILElB*U$e0D=TXJiW9ybm%v1W?69Dp+-+cmSU%_Zc22 z6gYNU&P6#zvw0|rV+Un|&te3i`%j|?;Da);IH#>6;d%emR}{kwx!|NL1x=Oiw&-;7<>me(oP}E=vb1Pf4sd2 z(mYdS-GNa7k6c>o?u!>iPiams)F}1#l$yz=!d!$AyF_F!K7Z;-!#j?4Vp5{}*%`Mk zMZFG;p0akGnSiERt0m`}YY6S8?3LX9BQV{+o5KEhL1t*a$bH?111Q#z?`4(w|40&zX0CKZj3&N=ZwnlzYo;6tCjr=N~Mn2<^FJOzG?5`ogcmcj@=$KaVtccucD7*in z$04cNPN0NhkJxo*7fy?mvqG=tT43$uPEvFU_&mKQwx1LuRshtL*k=<`-UG0vyM84Z zrX^bPxMGdGOY;Gfue!d!wd@YLye^%cyztErMGJ10nPRgGa0eqFN!ITwhso0tL3&JBT6gnpk`>H;ngYnKQJ1af;0N$4h zWc6=***eV`Xk+%YoP4A$D=L6-QZWxQNZIw!EM&|SrXI;__3T0UX|eSHyV66Ogf!r3 zp?G#;d}U&2kg7l`{Y$9@^31V6wt7Q)m)SmG0<&_wB1nQQUYma8%xd^Dn23`;JZE)9 zO;fz=YYjQViz=Q)$Y-nFTqW#bKiVp#X-l6KiBYvCn)6GNq?|7vso*fU>{7V6T1Ig&S?jES+}9! zPBvr67_Il@O}0Q!Yz`K-b=%+oi<>ZlMl1irO3(i#)tsi&g?4yZ$)DEL01!vr8z2>7L*7<%+nLq$823y3j2JTHCGr6PxM(Dl%ixeImEy07HQg#2sHT`tl8%uNbo6Z%rUHVI%{^^ae zmXlL2(64s|fpK@|XQxh^DGLE@UoB0os|=0@BaOtQr6L*~gA1SBwh_U}qRU^L)e_hT zV$&+<=%*nkSF{MsV_O}}jjVRM1?K+1?m+_Winbd{XtS|WNwgpJ3uv6SWEM{f4YutJ zJ+GG|yVy|#3k$53dL6~q4Ps5*h40EV|5l%kAKRz734*4-Qi35PZAkGZFs(I~jt_r! z_rNw0*B)?e^sR7r8pckk&t1|Y=~7_oYVB*a3iV)9==h~3Tvj&@k|CbuY;eEF{?JpD zb+e)Of#&KBM_*&p>tRnv8i|a=1s5qodJ&J&MJOW7!TmmW#f5QsGpdIZ9T!}>AFZ

1KsLXr`?9C63vB=jxV-fOf$pW%m8Ss7KJp}PQwsFu zn~L(O@W+fkm1oMC#JEUvJA9 z4Ir=a8p>)`7$NKvNhy*Rg099Pj{uTmYN7yHzgSSPD%2RmvyjGjFD>g1yZ6g9TY!S_ z@)T+fA&Zg=wb_c5tpJvI381w5JN|d}^PlCY+QzQ}6(s)aWwHD`Gmi)5-VFL)=0~&f z$CXl9i=_FDaNaAsy7FhWiR-q^8Y;j0W-)P5&; z0lGQ-Zs|cbWR|Ztfr;Qjmo|WJE_)g+t_&GlQj-&UF0C4MzH`TV;XRav=*K*Mx7qjj zsw$WQ1?YUIypz7o-EWN`J~08K3Rkp~h<5#^t{`giJU(CsnwLa=fTnD1AI+uh!rsN9 zA*1CGE)tQ&kQ3hI??RRPXs3g?ZW0I2y%XAKhPwMjuI|K!Wj{cY{fF2~*0+`|ZsEPJ zvht}1JhJpz{OTUj7UDy(bb`R$srAt0>IwWvM|jgizG1WG~-=6u1>HKDF6Nn1c3!-CG}viH3)rOJG_R~6PN%nynO(3MM= z*ZI2#_34L`lp~Rv!7iq@#O5WI?lTrEpZIpd!}Oe3FvkL0{y|ji^jC83gSLPW3-Jif zvSH^@JKbu0}o+a`81e{w$>AX22KlT>$1ssBLb}> zA7(+i68!wXn<9z*mrBZktETQ#JpqpSC(#lRaO*g|6Z{Ys!8Dz$HiKh{TNcGnH!>N0 zVW4E}2Syt!=`tio9=q+z6{^4NKTsv&q^-=WFHT|L=mh*p+CveqJbPWqrj z1wHzm*mYNMTL0NX4Oi!F*cZRUg1zdn>nOot%~?CkimBZ_E#j#DQ_1le@A7e~!K`Wv z?Aq-tx%joTV;Xkh&cOAia9>HbPKMlh^RcO5as6g9Iw!wtjgkgtM!B1`#PxzPDtwO; zKHc7x&Y|1m7<3G=>Cma%w`S@gZlr$Ibx1c>T~?L$AcSnl0q9XA@a)9JcGsr1L-Q??8{fk` zKYJa};L=WjT(~na$}&}Ko2+)c(PsC1h^iQo$d@Dr+(0R=uf}ucOJnVGH^(0!5=`-T z{U|r-0LNGiMGfNQd%{RzSewQ~?l%IBmF6AFv87@~Cwv%{ zBft_-W31>}hSvGZP5AY0M8#~ohC!0kG>CK=f8t%*9GJ=p)mh%PFdJ@9G|`>5ni&1* z2WXZQz2YghO=N{oYye z)1frS$6=;KqCY?tmydUxVYNj|-;E0`M9CM4Z@Yqj4{qONS(ca zx_G?Y!-@1<%)+eHspdd_VL2DOBKw~HoM}RY?aSTPYZaI5L1H3f8Nk;U>8aGP%XQ#8 zuIKxpTvxZ}32)zp_SFlbFCIsn+vkopOj%0u%}3odpkjINlqiK>QjKqUWkK!OwUg?` zEV%aaD^JyD01zgK{NV%d|rD_|sy-qupoDw65`T zNc093-TNd(DU`)iJ%QGz59Il!dPE+AE`;)oCue$uxOC9c?SB0n?cTSkT^CNED*e9K zidRYqB;~s91#wh=iXV@9Y_|vwsOb+Zn>J4!G-&4FuZm`*sQW0mh`4<5wYXl~bkzu# zr`y605ZQV(Pyfv?W==8yNMI5r^DhOwsT0fx|5B+ckDlB z(RDFK1!Z>K!2f#fHAyC8cBj^y@w)j-fbn}DUVKlr++5c9hseRCb2=$)PYZqu+{ zQ@}q>>sp1q%t2OOc@WsoTLdZUP4C9VKWpIFSW?Hs*S-b4afh9Vg7)n&f@37>Gtte3 zw*0#{`dEVvsrEQ)6TO6s%zI3Ciu!CI}kXiKQx(7;GZjHPy{Bt6EP6tOcFsv}BDw}Fprne~F zxlFu37qc(Wv91|`g10HVIPw8O?yTEk3x><$ug7!LrJ=pnYO{epl*?@ZV_|qNtt$xU z247JI^4kAp*Xw`3r@*1L=0Mow#n`B^WyS4;^Nh5Oh88icNsNm3_^hoY4eq1;JF<@t zfV_@=qJ|9aqtMTkDJ=H6P9M?9fbyAiBfWa9*e+0~*i&F3&3#E_*SW=8fm$lzzL(pSA{zUWB3Qyqy;c%+|*Z^?hWPFCii=MNgbFp>Qz++BF;45)Yq!pkadr?!Vfd~v+zpP(kwbFsD-Eks56yMwfn$>c1gqtM zp56!DKBxb|V06E}hNxcV+vnW_Z*HJ8hG4xJFOr;la{~mPByITfVj~-JUHapaRG;Mf z94#0sV{BsNb0wHWbx?xVso9Tw z-~MwYh)uEluqjwgA^(o9E)1ZGj1`ze+9p4Y`nvJJ`uQMC>Gu)wH z2lqSZfA`@pwlz84sWNYn!G3@R0FI%s^C2~*x4{jwX6vCzA}<+2jAcHszy56ene_Xw z6L??opTCxkNVyM>_c~wsU%k04Tgy?L;YIRx(W7%aA1_ZXRlDY!qam&lHH0#g*W&CO zDSmr*-i0w0Q0D+s<>LIb$X2Qiw{v@d`_b0tb#R6Y2oJcZZ-Y2`Te$PaUHwW1VX+&} zby^0soN7ly{yY#Wf38jNL4%alYyYxS;u_DBOA6*ESN3U?G?C!ITXQ4CK;$0fh1Y4h zFuoJ0?vj|{DOgqzDacaYeeF-qzw(zj>F}56kGZE^4O*puH4$KL&AtNslw-MSH|{-_ znqD!R2q}-M&o6UEK1qEr!j7C!|LYVUab`05pPvc;_HdgNbrgATd+!|`lL`F7!$JSW z=luV7o!z1UC&24{*SzI{Yii^FVehTun*9F1aZD^g1e6e_0@9@*%|xVyNjHdyG)R{) z6$z1$R#Lh_xTi{_>66AgK+sUX6(!P$GKglsq7Bnam4?=TpMP>5NNuib=diAVx=cymSU$ z3C%WuQ83%=sD~MiC*Df;@RvHcce8vMLH*q#e?P;_Qo-+?KHuPMeqci&P{r28_cxQ+ zg|Ktg(UK4EH11~!412MW{n>X@_`<%`DRh9W;1-RjMaa2FM+TcV_bInqXE0WifREeen7Pg*{`($=!uF0+$-OsB#r#M?)I15P@tGRB%&wXy2f|5wX9e5?F_Imd( zcD2^be@Kg3@K2}95X5iML;;$^tsf0q2L z81;j)mOaxJKKEIQV-P=#HG4Vs3<(c zPkxYqsx~oq2{UavMr0s9X5~2Ca#^?`8R?bVD@C+YPHW%=8NF6M%B$$DRD7ZSNu9SC zLJnqYK{i_hmCl*ii^XxDF+&)xuPlaY2s3QQEI?}Yq;wT#dS48Ht-MxhXQ`roKZ{2Qo*esR))#^eD&28&#V3p35+T<&(xSE{BO3EgeU~Ln}|qlVtWpo7}a!?eA7Dw9R!9H zWQYera!b4R%q&JNZBn)N*34vb(*hJga0!-sH8fhR8Ae5;U1Dp?2rl+)yXmath&+L-JshLho z)rf{MmNUsOSwBO*+y%M#*tx1(jKSo=jQz~lJXNTlEoyhYunWvH@!C-V7>XQM2CFVG zeK{j<57|hqg3GLwfkNNDc3$eQmDvBBUZ`)$L69Ik_h%NJKen3wR@ha2oENXYDM~x|7N1A3KZBy~{%1I#|B7!Syow^qZ%C!sHGZ_?a&l==53P z2dpH^C6$XG;=(|34%)Ye6sI#`wYcn)HIc#aqIN)HypD+tHlt;9TIo&OZpf0ZB)$2? z54-Wh8N%+Sd>srcT{RL8p=$g|=$Pxn1nZ|6L^;Hvdazj?^21gVT~ErbCCSKh6WMp{ z4BNQD6=wd>+0k!{pIcZ}ShJ3+jxOrC-ku?YUh_6)seEm$jHW7?{$p)t4Qn&hv*zPA znLDnPK%&u1HguFUvoH{5y~5J^WrZC>JmuwUUdTUug__Q9OMeyUC6NKA%ARG*C{GUS z;#~l*4;VAvZe@A{x`|E$=A^@Jq6N`Zr0Dc-3hdu2EJpyUH$4#B14|LZr9gMZfyw&b z=J?-v{~x6tB-2vqzt&sWvPyrDmvbAp{sl?`1lS1~rcUM<#M*?bT5BsQ%Cw#C(} zDNXucF&<*QpFiacbq3u9#z60!tR5`}i_l&_7a2C*`KBgvX3OOC1YGZGincn5r8w{ysfJm@)!G*rD#tDgR zpBs132-F~YzXPH})vfeQ+pdSxG+7QF@vs~n9_#^s30tsMRw|%c*{_b-zt-sM6R#ox zRNPMxHwH=MHO4DQklp@xG)4hrgEmMH8F^T4-1%32%|9t=`m#{f>s~8f!v?-ZyLulN zBXl-ExuxFs7-o0kF?9!B@vWgD$zPAGS0JnEA6%ns&wR8%m zWX6gV=it|`t%LgkYBAwXe0h=A#^CbGc_DdD(e$*$2SQ`}jR&=n8QNN9TqFl&f}FS| zc$~`$x0~*`dLhYOhvrIY3ZZm>=5#4=G&!+dFgyb6_5%-0Qw5((Ge5)g` zEnTXzWM!%=)fNjqrt3G8ey1&U&hX`H$6U6tCM~Pu4#n%A&evO~snFCWCHQU<)cP`q zXSbcypR)rwrF<^!%&Uj*cO9F%ZbLC!LGsjC?oZWrfo zXOP`V!#nZ}-&B(^C$aF40M{SKOu^VsxQ(xPT(A_`UHpSKOvCShf`4`4{^IqMa<~-O zuAIobAEEz`@A>O}J+hL+Am^$hja)Z`b%B4Mctq_uVcA5C7(P}u&XzP01J~U4^+FAt z9d;}!du8~l@yv(j9AA|YHP#|YVQqn^xq#psTO1_nAl~<{%zSgBn|~f9EN-C|WmbKr zypN8zTuhoy%F$fupc(ge34@yVu5{kDu=nujvAq3_004q@N$*zF-YU}Z@w(cuq1v;% zVD%HS7xibz9_kN}J)H~zS;`hFmc(-adidwE>zH1A?#1e}V>^Wq)|!*9%GTAZ1zBZ@ z==cH0f!(!N7BlAr1D3YQ9+tZP%POk?`Ec7HNR?v9)jkc0^uC@8oboT97t)`c%rS|Y zRM=@gVjQ^ZZI5wvMM{c0$}r2c&m^lVr=$|}%1vKVy={|mDqtnShF6pj!Ngr zor1$oW9ERm&f&(UFXMp#OsVqMPU67L4g(o^j$q07fMm)K>KR(Xn7D1MsTC;2md%1t z>07t@x$pS88<1^ak;?zX?D22o=ziAG|LPh3n~#qu{j46@|9)uqr;q&}$U4sl+CQ$S zc`aQ+AAarJbNH(+QJ`UmG|4ozUZgY}ot_AtBn8c==Q&xt*5uz^&&+Pi&@t>iIbK%d ztJJlK+{l_+T}d>RX6e`xv?5$&Ni^;rns83C3Jq9s0m$4lhy>pr77AX+Lmt$lXPS{k zpY6;nyKJU+0Ype?SsuyLY-gq&EnF8k+O)LdmKoT~9hG~*l*rj>dB(Cm814l89(Wg7 z6|+f>^A~Lc#20q|L~XS1-l~;OJ5~OQOBrrq2dA>Yb@xqu-reWK-?X9cayD6=5xa04 zC`C)&X4)oW=C8RbqXvAQ0?;%2XDs`#9{&S;J7Yrr_NWa~^aJlDeB0P-J~TQtb^W~- z^bX2bnOY>6m;KjI)>yn(f0wd1X9-@_24De{S!ZE3$Nlw`0{Yn*zbga>gRVygFJab^ zs7N=jHRCru}ApDK`P?LoPWz*q+A~}HMa9S>2sRcWZ=wT85zOF`{n|Zf<#T(p%ecxDL zMIf#8**fHP`N*UW$5Y<)lS*YoVi=%sFi>&!EZUw*pPQ{oLJ0wjPt_e1gM&AgDRu8E zp;-J4tJVgRgEp@M6~#w-fW2kr;4{Uz3$VX1N9wZ{3hoT{;;MPo-uH_&Cd@rP9ezS4 z52{fHXztKxCrs@b9`@VbIg{&ZcrxB}2YHI}7Q{y+aV!gfJ`hLd`aZ!Y*>oQHzm^!c zukjM!64@RzaY?Sueq3@MVkCwjicjbG@}S>>W@wU^*4@-ivdS3~grXrv)G>`x*+J`= z_aX3E9ZgleF-(UD&zy@K8BDG}ds~aAw8Y$_jtQ08jcA<%x zOQ&OMoK(`n)b^AWGvsy+$gCq5PT1=T41C1{yt*H1KbMvtzih9(0;h?K(O)S3iuYNB zGD`K$!;Lj()sNdM&;~60$dXQ}(}v`by$fwX(UP*VL5U)p=*6#iswKVV`1``uY*V|q zO-K{NTCOAz?CR`gG4{H^bA}~oP2^n&d-Aofczt{Y`*DkVNE3~R^tsm-99vs^bdLHm z=GYu9jj>$bw$Nc?!#$GN=yqWH2AW^6$4VYRVGsOYCGRN>_YW9(a(_8l6XY`cunYB_ zthY$&Ct0txvPtE=s!O@s`!OfN64VEqtRQ#E835S+p)X6XpR%ItBWIwgXmQwWz;!~* z*`6#cx0$W-N&i zR^2M$;_~L_EZb9eiC;#$^DIP4lo@0X?}Z0rK0aONJr}2d1?yI>5Md!#HN$d^<{c`v zpQf>LUTv*0d9os~WU;q}?vxEZ4Aj=@`o@#($70;JBIax~Qy>-wdo=HrZ-hNKC3M~u z$&=}%uV=S|2e3r3-iwP{8L>3$?UyRpif(++w!5$;xewCtxUkRLxll3>KP0^h)|n>Y z68Xg5B6O*J+HU;(h{@_dA_Ya z-)5+OT2vbIc2C>V5#Z}EIVf1}iOFuLRCDOn>U=&GB$FRIfedMlaK~SldNi`Ot-Tda zJG_Jycae|C(K3}G5z)p@8z2{#?xW*GWqZy%i#DkOB}P3;EpMcBxQnW%hJ8J|op zfkJOq)u1sFWimKg2R9-^RCoi2CM7kOYfYkZUo~N0c%6wfK*e!=wxtgu*0%(Nu~iG_ zJ9BKOIlG9IB2rUbsm=2fl=P3=z#sfP& zVO9~Y0dU|@Q^6nq}xljr1hf#-n*x@rbCSDRE*q8 zPRz?IxD6O=M-8*C)teGI7Y=gDy0wcgI`gl#FI7}HY&g1a!1NAZ*7ZD2tabZM?Q>dD zPlci2F(3kC_u}|z%5%)V;VIdYyfaKPhg-n!d<@^cryn`1zuj{JoZR2|8c0f|I>T6yf}U55SvN(6jV z*kL-NwoNm4jXL$SmPxq6kfy{1y32$!`*BLD#tG%Ogd9d<4tKWuf5N(EAdBy#E_NJW z3KKpB_5hVgCK+bga4o_c){a`y9Q#&YMI5U?jqIR?ZXMjb;NzfDzoXKE2}hQGKU?H! zE}LhlkdsSxy0;3jDV$ODk}lp27VO0zOGiFW;fC5U`_?+y{86G zHNChWlQ1$|th^=)3~HY}+pTfs0y9_7W}!^N3^(y7Qv2R7?QODnB}OIJTB~dHW_1MA zemBUPwYYJ`LfI&z5&y%V;)d$5sWm$SAydC$))k ziE9h2M?Oj^iX1fdI4#}h5nW=WZ)ig8A9^c~D46GrFjfvGPBvfSCU@t}nch4ZPFU zZ!EQE%EPuaAII^`)Hv{TLcb;8s@@FO%)JHpII zH6C6m)tj>IcE`gN4WQvNIuI$}@naoOp(M~@ys?N@W5F(K*}VeJj+^Ev?OtRkpq8MH zKXf2}b^EVvfBWTE{&5LZ-PPJcK zqdouXQb2~=sV}DWd6L`~m(xJ@TE$B=m$j<5C=Nj8GEKl%CA)u3PE7BMK($*noBJZe^0RhuxK8bXMNzJkhMsDN?4t*gBTrZ*4kt(5Nx zIvF2~e^lEBItCdiFKvDBw~#4x$uy9w-U?5L17bJO6I3T8hs zWPR>gfdtR+dGZI&2mHq!;0u7vWN1y#r=}Qi{c4L?S$rgj&8dpohv2FVvyl6wT^80f z=*#DOpp4Vi(5`_pX#k5iTOV6&uVovaR>iH_S?R%=dlUWqQZow66MObEv&BA_xSCzs zhc0ah$NHwHm~xtyKjkf6emg_aWH0<_m&~&2?rQpGK1i&&z6W;p{C|u5&fmo_QW-ltv?I!0b_;0(O@b};K-IgpZwYi4B8|wY5!Sj|xsghUg3_l$aZ32;%}5 zwVPgJYvrlYT4OA$Q-&ryVny(MYMkk5Q}~Y?~$gV%gd2# zRd6&)>(SMjbNcMi4_}Q0AZ;qbp+GF)uv}8u)?2;mW_6IYP07+$jhNQQ2n1sH4qw>m zq9U3E!<#@mM_@+HoalBD=C@JfU=(E_?ihTB8*bwC8QvxgdVh|<4=6>_zT&;ZM_$?} z=r_(~$063!a0FuDCFlVqv{=X#>IJ?L4}2bpB!V8(EK&xU{WdRb$+Rejw+4L@ijIVq zR6Z8e`HD9IC^FW$-4ylOJ(hxSsxJOyv*T-ToSNZXNZgU!!;iqwxpPvBH{p&1;;si~ zz{-ho#0rEZGR8lPr45rm*0a|Kc0894wDv_b)TPCNS>S(r7d;It_1zlu8E9?j2X^U3 zIc`w4zL-qf$J4Va#G6j0Uq++=5&>zuIh~ywR7-}jCr7+D@w<`Xqf6D-s(mEw&o;lG zfhRr)tUtrL9it`B+I_QD+a?!+zs55y`|1rcQnBbn90~k< zKqdr>PTdxWy|cdzz1eRXo}N668nCbuI7kdga}#$S$1n(>QE$HDjfY5L7$O%?Z{XvJ zIPQT98IGdcLuO?UPp@L=GYf6l7lv0k)|#sm$Er@Y?hxETuv?7VS?qZ%xGl97agdn` zWS4rOsBDh3e(>{B`eLhM*L4AzP9;LTRo1=WNot>edte~@ECq^P)Qdjn6OD%%P z$E6Fi2{58H7GLoil-NbCEiqZqo?1TAbX;NtK8*oaB{umZm}PAkdxo!xXdxf4{f?S1H={8;zqP#aQ<*dlgAD%0P8O2s7r{P z>E(`*Xz{V|rF#Y-DI<_|3`_cC3?(dVv>==eSD?$3O#mg;mCiIv*SFeam78Ux9*Kq5 zt>w(a3Ibstyau2Zl+z-qfIie3cc$;&t_@1@#a(A*h6yEs6Q8`%&d zlko?Abplf+)kZti-h11*>?H@PR~7Wm*~4eC&ap9&9dp4f9FuWP*Zu~N@(BR8#d~mM zDBPE&U+5#S8h9C$ok@}M5j?{W^8K#GO&N5n>~F@AqhJ|k#Eao&cAmv}P+zg8#lFp~ zOywE>{kK!h2Yfn~93FPa{%vO-!RYck!};S4SLe6D*-JoBL*(QbWW9PgW&A3LrVRQK zD8{}S|8g&yhyu@YPf+EH`iBPOzop;lFV^Z;lOb?Aq?{wuAwTT=C#!Yx_y~7dk|Y<_ z{2Yfp4!ZRYDMWUBdt)?|9ACv~Dx_u8b`)|@j+*431?jV3Izh(|*crH<-n_22nf;07 za+t+o>%W&$kcP{um$`j&n^d(*Gr2~$?G`AhUEt|Dj-miWaqRASGSH^&oB$d7ZLi=j zwzmE8N!x+8{W%s7Dqq$K>38o4SBKT2LuzllHsU0q6Pr_Q>YxD37`+Q340%Y}GyXH9 zM>cQI$E683SRmi|ev(MsMI@ES<`7W}a70Fx3weBUzixrAVs>_C(Bh_rjSKA-!4?42 zE~(|dICcE8N4M{Ukn7~B6^kg2(BkZ-@uD5sbw=DPw zlx>mSfsrk&HCbO9qsvP5L1tbiJYi!k4xd`qu87IY9kZHz<%VCgnZ4L!rLhoX?X7xo zXBWqYh}2NeGFatZYpnJ-+y^_)o>eX`xgGQ#11sB2%FFYRFO)TVH1v8e1O;Lxg%^>EH9}4+Aw|J&YMS1z)i0VdR5xw%LF{IoOHhD#E2Oe2 z+QoO7%`(2SL}S9Oc|0{Q&7MtwTIO>DV;!MFlL}As@QALHt%7l~q5;&|m2aDDHkI|< zo?$j{38EC5#NL^Fs}P7;*pkhbcOCPRxC~iTQp<=mUDy~h83cRtJyDJQ>L9BuP4dT7 z+P0UU*`xw><~zP7g)7emX{-#LT_<%^mRD#&rM)t+fM}ItZzpFtxJ|QnRAP`Vhn87t z3)<}aTBGOv0NXjmr}?>D&G^jl+R|*n`&DbjDLY81#tU#~$U|r)Xr@B-=FbJz`%^0E z8lDAWl^sRR0o|XhkC@Yj58d9y-&cWs@%~UH;L^-cW_0$dyyw0JL_RLYMI7Djj+YED_DE!DoW2@5L#!(| zg*{S8tz((3oSl_jehvD(B6n|jD?Mvl*yvNqxM8WkAKgst{ER*-wPX^FG%wLq^V>49 z2-`E<3#!J*eV*P&-eNg9;+iv`82(r$Ovi2|KnoT=%Q}T%eI%l>r=7dTYp;*9ZLk}t zW-s7`r+AQlv{QI94udt8x?RLg*NR+?Y{kNRMk~vZ6y#r~H!=QN18kwD$UEmVp6B~k zorx7C+YC!qI$pL=MH6n+(_g1luRNPpSkxb(@_E&+%ChPHS<)LpjC!ThCkvO_xHq{S zRXbnqC8&HB+|E_X6=QFdCBg43k_>#SPwO2ynYLi43vh|2#+{e2>B6&>Bs@x}Wl|(!1qH4|B`wQa__wehYx>D{@y>_z& zDW~K}twkJxxPz=D9f#lwj_Jba8Lr)y4m_8Wp3|-bw#sg7Uy9f`6xAzdi~6%Mslh_i zHXEW8yB#;R*9YPfO{b-No0Jgt#Ft{&ileONxfxw|O-QvfzY8v;0MqOxmM3hHH7F}$$=JoS#oW{rUx_PkB`to}&;qRD&@#&=u znb%AC3=M_6@QX~4M8_B`M_ zUSIJz9ury@#0(BE`*oj)+j|WkCc;K$BZUFVtM(3i5(s6VnLJ)HYQe@nI$c1UgR_ms zg;l+Sm&Waa^sXkL$h~<3NIOGUb8$r!5eKh8-W9%`c4SNGCdV!b4(uvR3V#@^>$*;R z|D93c2MHDmF&2ZemScO6&I(K$7UXNIYxWUsPHlCL0yCT=nptp@XT*gi<4VKO`D`8j zG-+vXUiQ>5%hiy`9TS@+CN$n_pP3zJPPl)?y^Px^zWR@pk8z)&r-j zjZF~XF-5`F-J{^1n-|8!FeJs}NOG)MFxez~l2UzNY(#;;8D`F2&g0DIUCsjyUsLU$ z)6Xm2XL|SZB(&Fzzz%@QS@vzBgm*trg5QtP{doU{gPED=xg;KiwBFT~y*WYDTUI66 zq-pVriD+NjqficDzsBzxY;3ncXb{`!b(+Uv0N>Nb1&-t&?-v_ zwT~Rj?50*fU#K`Df;iYSDNV2Lcnbx-F3S(X?9x`}zG6hPWyCxE|IN zh>o=XTzHT5Rv54#ZXEy$b^nKrMX<@rc`Frj$#B+%0!Lr-@4xh2?akMIVSUUbp>)ZY z9tT_>K&{qAz81R?LID+U(8o^dBp0=-J)!F^HU9jFSb1BR+tgu9+sAi0^vOs%rd*B9 z1bFD9qVex;g7HRPkTJ5^M#Ya^;hKG}B2B4`O;m)&iwc$%F$gl}UT=4UeAeL}>TOo91N0$G>N zym2OxkSA_?@O(8ARmuBaI{wquH%)Bq^;Qq$@u=~pD%Q#3nH}y8*~(wX{%Seo@_o$A z7|)5(r<w4S)$fDJaopYFoTBv`KN!wx2~nxx3v`MsIXnZ%ed8mx26TaBqCu0 z>O)afe9O~!YYF97Qp*-~mEl8qLgSAiuZ1OXZC)iqaTBlYeq|G?l-mA*3E6%%6{B~+ zAZbhWaLQ`50d+`psjb@hV?}SW>7wAIPVd;V*9-_|#Xa8O0bwvKmpU?z)x5+=&T3BCzR5B2~EJG%-iYt=)`Nw61@?T5Pp)`#q zmHgF((cS;cKlFc(tLO)5{qOdrBRx~9Jc zx0u?0lj3Q_x4jp*I!c>AoF2d(obq#<{e7bbKHo&DqG=`rk;&;Lm7a!v zTU{!k*PCZvpvkj{gHYS8MlM|XidfJ!H}qG$v127QFwL!zzuT_tbk#87;x7iOFHNm# zY+HDAG$F+5i8=`l8Mis}(`q}by}Ng*%l8poXIjj%nHL@Nxg8a9_qki$f%o%df4Zck zw*(_P;j{W$d$EPO>eFoE=mvE06PBfE`+8Y^|Kno%DW^xwtwhXqxl7um`^xkXUY{Z3f_uh}+fQY5!o<6IjD&g#ZrayzjjNNSems?=w`%Uj(Z9{*u<;Si&t27;q zf3xtv0kwaXjsIVHj>Zl2V!XT`V#e-ool>3ppqq6{YStguM~I-ep>UjLcyUBnip)r2 zt;DBLhf2NN<9=UMy-V^$McB~w17@0&A8%GriBc?e6*&Uxr#M|FJQ=d9l-_ zc;WcwdHqKc^0onqHMyy_;&`@z1+hJrsxPM+X~)4Mco0!pRjM+SOY5L8cpL8sj;E*R zf0e}ki0ZCfe!r68XrV1(gE#Ghi|=d^5uWIEJy@A>6wbMAlViT$y=bsUqcfCYot8ON zUujD6EfxF%`cXKHU;pwfA-E8Uhu0g-Oupis7r34Xv<)l(vTF*4M%rpndVASr%d)X$ zBzfaB#WzaOkjA{7o8D`)yKmielcAHcGi?_sJ7dzH@5rW&)|5qaVu*O$D?woRQSAxI z^|&VmN17$~$PeY~RHTZ)=P%qiWm)g%k)WNWxmaSxm(v>nvi7tt#2 zQ*pa-6Osqwu$A2U&4n#15~r~G5(z!l4Vu|v5Fen=ENqQ?7hgoXF2*hb7?8+aEI%~H zz5qs6D95^yq>!uTfCqY)X)~XSeJC8IiKEhK3%%K(Gc})i9wMUR z(sg@>f$(C^J%?`b;|BSR3g`jC>}faJdm}+DJ7rC4cWjRATE>N&`IrTgplk2=&>Nqg zRhoI_o?0^#MM?C<$q9r3c?ZN*s2I1)J%Z% z6%w1`_b+-OO$pgEG$*vi_1WX@6mrLTleFF~FKP-Wy3QD&wQlLL*5Ps@JEq9*8kIV& z7Z--HluVdMV!vbL)to#_2oc!{bSU@eCHY?3K||A^%MYq=G@)QT?!GL_!JjvinWV?$ zzkn?8Ban5!ed0${^e5q~f1^hf2OZ`vzJG}&C|h?{>yXRG zQTHBU7C#5BvN?Q!k+q)%ovODej;qM2$#^T5dD>nx89IHfpt+>7g__?Z*Z-{?C-FKxBw@r;@Vn1fS0 z@{uti=$(Qr*OA*T3?JSTIyMT_>{z~e;Gbu0`%|}93~=~_nD0ktZnkZR$F>2g&OO%w zg1WY;akArnA6^zCpd)_v&~~hxi#ZqF`6Mnz^`1K!0Uy=apkw{E3&nit?)#bPJ?4Pa z_8&B?9cKD}-2SJU*M5BeKe}hB!(DN5pR22nP-A$&PkO8QRPZDBFT5{&LFM{L z>Y)@8Z<6%LzVcS60hw`_PlHN*TV(14=x4YY{f{Q{U-f+YzpncF;yrZRgO1@|#NGQ& zJH7C;-kKtNPN~r#k~@e<#r`;?HV)W30q+;w6_rKR2hv2l{CWp7U-9m#@mNo~xk`j~ zJzBm3X)>?BC5!~shu!-FcFSzG&W@KQ)9%d%PVnc5DWd%nL5|4?7uf}bb^_Pa)gfkC zDwUm-YjxRL1sg1bPV)){aF+Yejn39xxW%`>gJm(qU0pDt@|O(eeW+$mfn9oCk2PiFbnG8o0Zqs828h0dZBjDX*BP{1DN@~Y_Fbf?iD9# zfUFDPE!2(otv6VL9sE4$BoCEhD$Yf$8RR`-=z*a2c!&en?8Dc7XbRb) zhD~bAXRlu&=rOr9xI8b1y@Y`?x96>?mSoHAWXq^N*9zVY?uDR<&{Gu_47HRJK}Eep zBB0NW?eU(ENZtFaTt!#ofK?-px~EpLI^pBUZG#uy66+kkvl=U!FvbnKQo;N1@|p>j z+}@|hl0RDnukscU?JaP6;Es*2ci7vhNcgGdsv2nB$X2ka8x4HKLriAi1LFo z_N1z*v0%Obf$+E$u{SjlI4;Y`jj^O-buBZ6w|j6z2pH)LPQ7l}kR=w%1r=g#nmD zz#-B0{a8^^D{oj_j4*TvG9S5lmH$ENm-#H3uDnHg;>NmIN;!}>sv>Pp4sZ%rj~3u; z)*p-0&38o5D5>`NednTE{ITQH2VH$N8&sTdwB79l!LaISy}J+5aS8`)5rc_VW1XDe z@!3Y6TdM^_Q7xBtL(Z|ga)A6IhUFq@NWH+i(Qyg+xvzMJ_*R=sI<09cXWrwT#G}q^ z`vbq*6a=xreAY4ZqAz3a{mr9?t-WP>R4AZn*eC8M2S5)U0WIU3pm__%ntq&vJL@9% zp#~w@+d&cM=kN-58UujzzpmZ5~xhu1fG8=(= zhL<|WQ;)4TC_kL}5_BW~daBW-VqW{YNrjS{kwUuDu53Z8rg!BV zxa}+qIKH>d-p1AAt|5~-)w6=qac5Qdn=0%f$#z3l&$!#}2@ea8coaNM-y=eqrM(!V zQh%wsmZT8i>r3i>C$2CNHlgPJaK|3?)F%H+pkN;kqBqJLm9EW-x1mhE;+~qinNuOR zmJ*brpHOt66*`+VE(m{yIboqhAI?W>LLX${FCokS5#n#CM6Bpqpy_1#WKbtCp^QLl ziZIHsT#(GHMRJ`e41aoj4YL#ya(|uo^^9@6#<}AYNwkl|P_DS^W%QaX;+euO{1zHH zrmJrUTh&tha?vI5(!c*-p7GhijVNX-d$HA94G_yUhB9jT?RUR`_}yB!T_r_IBEn7})E(rt>f2lW3ReN0+g1 zHK8yO@+FBQf`wY;dsl(9KV7DLry)zK?BL}m7!QKMroKAKx^FYlj06@JAdT@(l)W525i*)!s8g@44I zEcA4OJvM`p$h+Qr?8}(#4?NyKQIq{M;e)>p{b!F0?A@-hDV}>$khTZehnV&zBqa0o zAJz!iW2BXWLvXmKAvS&EAl?5o z5KB~K8>yRWouE^cJX8`=0jRMc3c8ONfRtB^+ymg(%RC9VLDL%>qLnlM_&X6*j%|ZW zK)=?h;qbkrrc`~bnd!>@?EztKI3-Vvk?_?_;_OJAIf z0MV}MkU!EsZ175!YhU35uZ6r)lS@sAY2+ih&5q5sfHt6;wdaahJqbEmhooY~8yY&P z3;H?M0znTVQ<9eKR3NEvLvy=m<)Gw)#DuSSSGPJ`>}`6nGoz9ldR@Q&wYiEAv=gav0@b{?{6vbG!-N`?Y zTY7W4_L^To+-(+#%JC_4Tek}Npn@GYzYJIWE@%piDJeq6m$ivlp)Y0T^-kP{!h!Nf z6-BWF43>0I&dZL+;^txf?$?GI&zKA>3pxbD!r<(wTe*z4^3QkBK5*TTb6I_fp-&xN z7;xkbPtYpy6LcQu)ZP%$jp5QMO6yJvjoiCT$}+{mOS(K~%jOJLNuGM;+{-mpuGvQ6 zSF7j{ElR7p*uZoh>_d;Qc)mb`1NF`K;KOrB&M|qIY$mEfZCxv|8&z?03rix`8|oI& z%Qt+0ZiCcsYLcp2rstZJ3q0OfBwi09pY57p30GXYZr_(OJp1CnpE=tVZjyTEab;-$ zIaVhxE;ZYECw=|G%B-J&T+N%*=d}Kv?!e_WO^$fUv+Sa8pVqvVX&*cjJmbot*G1-$ z?RF){hA)6jw1c>H!2xvML(Sv$A@!ov|1g&5EKOfg`;{>&s`LJ|%uv;aLVJ{v<4+$dA-PX#cZsQ4H#^S5$@kTlxZI zDoAq+qX?v}9nCJMrc80xElBdTnL>zq^7rj~lmLs~+zfnN48u~KP!Piivolom6o|VZ zROk5W=?&-2;1k=}8NCUpn3g8I{#4ZIG|f=xW-pE8j5Fk2PQRZYWkbIZQxb|F&=54k z10bO|9imzF1IjnJ%hj7i`l>+Fe8X^wTD3`0ZXiOTZ6poKx$n`O88+`YcA}pR($P1s zQM+-{fe#m#EsFnYd-Q z4+FbBy>|G0mPB9UR6pr+uddMm(*pIkplH-PE1<7Pg4SsQBqN6ZISHIUCVL(_g*Toq zY_W{C2q$a(Kn?KL7X6pPTbzy93b2EuIc~yBlv~(jraQxlnqerB}xsudH`1s+Z6}`m6R)jjoJ;~4-`ccA$iKg$toWv1WVq#7XQt$RuKIsp6ihx zul+yg`k%*(e{`t-c>L43Ces9|w8H*n&7qwWkrz?fn5JZnjB>9;ljI>D#FwRWEQly% zH{q7sLU>BmeARFjWO=2(b-2i_kt}p4JU^@>K0?*6j8-mn!9L6*)2T+P*O9hs zz7skd3gyBd*&_SYk>wQ85L|e%jUm{HYuG;xI%c)bQAn5z zrZ-tO_o+7^=?hT6T}XYaP0qVDzc_$&wa~^rH63G$IOD$^Rd%zz?9z2gW|nna^(KeR z+D=4b+5vsWE3Wq3O@0gvR$ByG^`+T}=Q?eo=0Yj}zWX2Rn<&7nPV`H#_$aD(f&7tj z;eD#;;`j+2fq4$&Rnz$odm$S_xS~$k$!`S&itKZKiRk6G(O9+IMcsGe`m91UnG&|J zv+|nRkf8Zu8sW+keVTa7f+L>{_b$6hitVeZrJ|!``ak4pXW0P%l%sWg0)D0do@jC? z%_3VAAc4bi)~#PgvZOtQUapR@58OB61s*N5HUSY|Qt4BCEp}`JBR}Q4c?1gfC%e@q)8QoP((mVq<4@aO79>bMS6$O zdsj+8q&MjhdQYek0^V=W=$B#X7emsQ^NuIGK$^U4f4P|E3( zuJq3okmNM8e85|pWf0ke)yzXDSL^~cYlGPj6_5Rwo`#ohw zGiyeG130)E<(V6GGN|+yr|b7T%P;Dj|E@_H_am#%&&c)fd6a)uoqq_{f?)fW ziFn0iULF&mkgMVaYQ`>3m&$fYDl|-B4_9ufb8;i}#fKrDqy#XBIO7L`7`<}Y(rSB? zEsuYAMnA`B{_7V1E3fe-uX^*4C_~AWkugy@#`R%L_b}F&rfj8wDz@CWhOEy6-ZJx> zfs-C1tO#^hG^Dp^BW{U!TT*KRJojYn1>B-5pH}eq4D`Nw8lKNIDWAM9W z_*KS;rj~u!({;5#MV;w{R@Fd7U50=MF5$!VN-5!044yF}I-L6w$lFN%B4gC~>mO&d zN*Mr<850Je$gLk%Ifx8{vZgZAcSX#>JYB6c8mX_&ZMgL!g~n$h*0mjKBVE(8cvLkN zfSG3acW>1ELQ<9LH)NH2>P>PRGxKG#AjTt?Cn`Mue9N)0OMh5XW`MP(V^^v3lGv9$ zOzkOU2bxh1TDQs%@zQ$6a?J{1H*^Y!ICSgQ@XI;~qBwfwf?c;)9JM*$#XpKBN}DYW z?r1^3+y5!tm&y_yF-nnE030~80RH{&xWxt3`5|s7{=-^;j(+i-{>b;I{;qEL!&v;V zlE3d+e(}uz4q`q(Nogm0C>=JXWDk;Wcb*>ZQ5>Dwmi>nI++7@Od|ZZ5?-VT(K-|6zW2J7kE zE;D>E96Xp)z(6AcznApnp&ruF1j`tOJ;zm58E|#0pFVp>sCQMDJ~pTvI!RLgmDtnQ zrfPrnXn11kisBIOLB8PoFw{KJ*HGyD2DB&iy7s5Fhhc|Cw0_GU7-3*d-=2jDQlexY z;XV5ZoOJNb74-!g2bTo2TdONLjsx&B^0hl2dE^hBKISM2*@xcPDKd~f*e7m;oF>oR zZOzOW_AN0pQ{`X?dtfJ^Cp1uKUG_Dm_jG*Gllzpi4>C2OC(}6Yyri%G6bkSb8Ufxy z2Ebb|2Y3s`znC*WN7BBR|EAh%(}8ptbNVxc{#4tB5^#%aRqZBQeyIs7RSj<#N)Z#k z?9V*7>0uvGpMjGuB$^CJrg&z3k;yXa8*4i>)FD5apFF)@4cxU~rA<=QFkXG@C*IUL z0}v;-ic>Fdt3t{Wh%RqyK$+vB^WJGMB>d0RLzCs)y3qM| z(C;R_E5tz%$IWrb?LfX%*ccSGxEoqIu)?;Z*wkz;O|Cq;@Yv#m|GcSKUUWE0CImxX zXzEDQmX%IlnR}oc6=!Wc&dLfHkbdYj?d7MR|w#%(;2BOabn?2L@$&dDf z5EQ6M1`(vF+Jy5nk(}{Od-Uc)awq664&J8)P<8wC{+g3^kDs9@Q6tPVYr_25&{I&`b>DdMOM z8|ikMSg{lHRz1g(pH7Mb?(mx(Zx>HDk($+kIN0idv-IFxOuom(3DbAwO;I_1-2uua zd&*s-tCF>E$3{6=9?GxK{-~Mk6bDUIxQB+Zh_0p zv1O>Ai?QD(G#AjLM8BTrxc#KgyLDP8D-+84*_%K9j(&ofD1FA$Q}MT0707QO%^^!o z?Y@0zl#_efbb?iRF;>^2;}nnR(JJ6pQ3ZklN7QaUm))IN8Ru zSU~Th?(>GZDRAq^P2&RXldLHgvOJgIcTe&0*ritByD0<`KW8T?bi0S_iI}U)<=_ia zv%n2lDh<#r9m5fNrX&e=`=qZ4M9I4W?ySk%MnE^jJzg|(-}88*58&9k$OG}p!?E<= ztONSKhYj5%+m4Doo|zH%PF}p74=}!-|HOX&-i|@_sOjXu6u7qZ=a26H|G3JJj3zs= z!{0#LWeVK46YHC%B--6`u+48K{kbE)|Lv!XOW%7U?54Z^=`kO0XXzhT@m~8hvM-paGThr!c%l?_l2{GtGP`QE zeQmm_?;A*XEu+%c=q6L_z2PU3M9-$3d8O-c3y3V2(Wx&y{K5vZ$Al!Y#SmBxH!{FFnROuizjU>U;- zgz)0*F<(!J7^YRw2u7_$i2%mx{fpq&r6>flYQ$;_`OtHZ%oAp>p?9sn2+7odzl>(J z_1j-ZYec{YBV&<_uh$lWZ`+}hBrYx%oMa61?+UIrLHQrJmKBy&1h)5ppGY$Y z+y!VltA9OvbY-8c%YEu_r|A9j^!mP>Qw)E{%Kkm8|G&G=50B^j_sWtd+n{Tj)}I$o z^SXYfrS;3vJlg{OAM`2T#p6@sS$FhfkF(L;62EE6|GNkCEZfh&>Y1^Az)8MTv7L|f zx5DQ6zJdOLN&Ye7q&Y%eo-@+G*7qBVh-WN6kLQoMmf;-9b~W|z#^wd@Ke0G-vqK;%oFpP1)$t>E(N!?EMZ&O<=&8>XYCh~ zxOy+o`Hm}rBR;^ji*i=9rHj?qTpx$5s`%UySmng4oQ2*E1bGj98da)2R9&Xm~6&5OKxVIr2%Og}nn|ff&A+*`D$_4v~k0e9qHiypPIDLwd9T zG4vKjRSLoZb76tl0(Q>qtg-0=6E@DLTG+`J&4?l(F;O7fTmVzRWx>94>QY`Q_8`aX z{$jnBB4QG?geVWeF zjSVh$`-~R{n>>iKmf&u)Y~xdFXYsusK_V@U%h+BcKLh&G6;oCnGu2c(Bm4~%w*EvF z4QZuO@P!DNz)_HgQkZIqo-i{FMhdGHhLL)4!I{3jT5vZEV+=UkE}A6)Hp9;q!;TF1 zDf_gXcNMEYPRVeTIk8yj1NR3W%aA_OeWAmxo|(D1=D_#Z4RS2MFa@=vXlZQ64a?co z02^(W^u@cTqy^p#{t2M{ftV+@vd26F;jknPjsB$0*>FN+rzbFc+LlGYmO!q9v>6lm zNkTR5&zZH9gQA4oI*m~#fiVC@TmHp8Ak&A&sR^n`!JBGxHk9h^Eg&}uPBCQ{n)c!G z^`!AP`2m*a$;E)&QiU#NM%sUc;lmjX%Y|{Ei8=SFxY~M_&N&<)u75w319ir$E{jiM9)4NA@S+#0IoG&ticUDctCt zg~U4mR(i1B;{M>}{X23Gw{wcPrx3{``^R#vjZO7oj5(C4W+>u^qbK6sPq4Z^T6;1B zB8b;TLW986KS_h za{?)WJWqW9-&oUT_4kGw{;J;p6yd$UdZ_O)l7F|{E#mLKpW6F=W-0tpzvTNV_vYUB zWp;Lt=Se(}BBz~^@JqoI}`XjLuaBlydL6TpohxI)Sn)Tf^|EIF+{f%VdKNUCm zB?kU`-(zQE+L|`~PP;YMetz{=4}$fxJ^#UyK6?=O@8!1p1;O?2f#u)-c3xs8>|}ez zY!Ck3$M`3*9sXyY{K|CyuX6}HvCBFZVVbgg<Mzy$F*$# zDgWpDp;ve(nUsijV|f$NH{}9B?^OBp*o4*_!YoE^V3+|tYaP#zvIj+9eNSF*oa&$O z%``cB5*ngXh)Bu-#p~YK(+3nkhBDt?`JB>!O4s;`W1OvviB)Yw9ISPXhN zZRk`9HaCl{-`l+6xx4rc6!E}h&4cl@A0_TnaFXA3`sr9^>t+4dM)Py)$+1+u_ma&g z{sqhRpI`9*zMlY=&=`%CyZr265`NHCy=l<0H3epTYc1GAawBk}K#3IXc}hmIk-S&L zd@P~r`;&#*=I!e395NJI?;Y^s``Id~90WCe1)`lDX|R4t%KsGqWU2p&KB0)!MxGFI&B3NcK07(Tb}_ z!qV3!qsmYcUdyRt3Qn7-;Q1``O_2HzuSDiAO{xIY@gEf@|HXm$uddMlqu28u*QJVd zoK;5e6QcsK8hvppU4Zy^$TQ2&G6)$t(5n5EzS79G7z^Po@Vm6=RHxttJiT-=AXA2K z5W-zeb?{mx+Gg&UJAIrzwR%V^Rr-Zd8rwqg3M31fhja=|XswL3(H#@>g=KdixUEZz z)K|4jX>4WK0xP7z%f9@KS1&0OOo~=reM{Ukwe@89H~rP@$mqVhm3b*Cpav@*qZAt( zT(aOMDM~~IRb=?fb#}Xuw%0LIp^)Oe9;o-o9)X0fYn2~HKJ$K;A-Vf&jc0fGB06kY z?GhsyBR$??t@Ej3Q%m7p-67&QBe?W@d<=LR1+iWVm*cz_Hq2 zLHfRN`#jnqH@~f(W6la6T{Snq77v6wQDW>nGv>CBG?vC^=^|w%*l-Dn-@%%E#5qSC z@UQj1d{{VI^t`H4Q$b>WY4Ux|jpOF|-4v2X|G=gG&3TkDd55y2gPQ5>-aO1jG>Hfm z8e(HJ4;GWzOky^BUs%5LwW_8PZ_&qj&Wjpvs1Fag;wVmqvW!E($dBcE?oow-?FeVU z6N1N|=1VAR3#jki#3c=vaRmXIFK0TjQs3&AqK-5pQPQQ8l>$un7Fnch2L9SuT{vX9HkNKhIp9ddA?!)hQz<38g4D&jS-0pmd(Hh@+kYLxR=Y~FEIC&+_ALiphYy6N3cRwO21*DjGF4 z`k^`1%1ap5+(k5HVP*p3TLCVadTda)ocJ5_f*osOAC{4Y`UuOgOdV&3{kpD^!P|SO z%UWwHgq`FEIwbW}vy_LKNIVSdQ842~68n3LkXHO~vj~|AL!2n)0W-)R>kQ+6^;BEt;zN}kuk%Wt zaN4<2bd}C!*TVd~o?7~}+9x++7dvxqc4%ecNpMa+brL;smp+rVy4a06pD7+Tw|;VW zd_cZtbIv3be<*ST4+jT>D9@vHtAl>HR+)73jVj+GL+c4$XLg&uP=uFg)f1$NKZodT z8-&YCl|0XnidHy!!Wpd)?DO-N{0S|Z+g$JXRY+_W5v|5&f$qeVJd)hdl49P^9G%~N zDU>I3vM#H+dr!{FSH9GGKGCp-t+kwn~OAZ zT;s8x4qkqrzq#7&cAu^P1C3}1IL~W<3seGE3l%y@Ij;?oF*ZI|aF6r@O?%v)$}3Es z`5y-fu>;v})Od^d!=_C~T<%fL-@*~pF?uyaO?}jdcflbG${>g$&ZO+g%r!k?cz=6r zHnbBHXPME=!7R>OZUn+hO>d5zZh-q^7z-!gtJudy3T9V>>Wd%Ng-hfzRIE@g+=-vg z%zUYZdX$j%q++BbPs-%LV%ED3zw_DfGY3iGfH_n5=o&OUo?pvU}!6ZiC+ah_VnI|mn$-3h3YLfg!#MR7naUf z>B8hPEnE=Ww2kRGrt$lYK?YEOtXput|GC*`qkO(#rB4@DK(iiMQP6D6J>Nq`lw}ZQ zRRCWAt|r-sw+7WOBipVnL$Shn$GwC}{7$dMwlZ7}C=%Cw)H&XB$|D8lGa*WJMS9Y# zK_ih?ui$U2_@Pk+dTTu9WX#;PSEDahQ%kf??o2f^#EDs)&8vLlbwLzMG0-%N@L4nq zejBYJPKBScJ3P}`4Jyuw5{KnKn1hChD3uIs(e!mYkwzDqC=-By<4*@Q5Gj=;*j<~Z zMn|NW$5Uoy(!p+75r$V9#t7hH1q#q&d5N(r`MK`5m(8VGPs@Ov*{BPhr&i2E1<-~7~Uh^b9hA}xKl_td7mq6+`lWi8L3n?A7|MMC_u~>`MVlNZw^K9ja)(9f6Qr! zw*cFn28(&VpoXk22 zcH`lTXx71V(90msAROgJY+9-7+Q}e$0&IxhZ)LGWZyQUV0{7h^<_y%KlGKM75XZW> zaKETnL0i_^j?!~h#ZISNgdClx+i}5`+vvdMJ(p+5rq=ufDqUJ>6nnzNoiMWpwJOJL zxSq}ic^9@SW(dtkT$O?fpQ7;m_RSs?IN6TpiK-Si%TicNEuV)eA^jfEv?se|>*<>v zjKo5n4ycSiv%(cq%gV%1LpE?K&>p*ya+}q~2ZUyH54myzK`JYtRi(3>6S8Fals9&b z?@xaqx5KIlGgvdl+7Bv37d=3Jjxj6HgQe6xfAW5~(unZ==kr&pRC$%1;jOA@I~iYy zym8IYZBE0oGM@?a@*eBDutVt!5=!kg^d@R3Ctr?{?Y81o?QK8fr?E}XtJp?9R8Lg| z5T_)=K-wdmhRB&##oMiwyXh-OJB7(^Or3AZLJgwCxLkCW8(cYDe5;V5y()vOel6PF z01d{eVxdB(>%|Ga@m&v%6)9ZJOvv4lgW`_^_1!e*cw`R~Uj--INs^A69XOu1GlLSI zcOe{j*LVBfaz34d_&%V;@|%aY-}K>A>gp>8H(BfIs{^N}uhq<j!+@MGfSjU+efu zB8N}QSDpusOl6JxZO>X`9Re=3M?;}D+DR@-wLWy|Au~G_XlH!^Rmu%T^`d$=W9eVM!p(0$? zN)*as*+BiUgKxjehm&E>AXs4*+(BXaR2bWB{(9-l8*>ZDY~NeuPqvn7A;#dPuAzmf z<_)1)N6Lz`dG3G2%m2-~k2yr13|1K0@*Q_rDwcQSPD4x#iyre~b((u#e#x7TPTIcP zbo@e9JBL@tJuaTLHZt%^J*uJ2S4a=m|K zEqKeWOVi+NeQqlZZ|%Kk%D^Pdke!p@R`I~ekd{JW@0{1{ZnE6!=C+09w0&ib@AIng zHm;Bss!5Psr7#`ohy6nR_QnfJq#4@2$xP$WN>BcZp1yldU*^$o=NmwFnl@FwR?DbZ z@9RP8{e=nL!{v2a_k4S=I~P0K3>A4URLbXRt?{C(npGyCuGh;yy1_(0Uvr4{`dr|Q zH^hL*b#{_Mjs|j?c*|2`ZX?z?IW-mDO_TuAx}dLGkHkbqrvBf)AXJj}d`V_m@$6i-YpUhm3eH1oVT3_eG@w6mRiMP||f`mvYbpVho z+W1T-EDEeo=zPTG*Rz|_?eNEL5n{8qyr~Ei)ai`oX9jPK*SuG>pCQxB4id#G7;9|8 z4imr)$8w=!Tw8dQGM~yExPj4}QoV*I7UYpk zS=|*HA8yDrJ-s&i5@LwBP4;EnEI;cWi=(5m!i{G;Ao?lfc`h!{K4yJIf`D8Km4d?c zU>fFBzi-H;!}Z*wn)u`i;U2RPTr9sY=Z{-Bz$zBnwR`XlVn{M%a@H>CDn!aLsaJ;C zbZ*&7;;cc_^Wu}@ObS*9SC4#Zm7v1W&XY!@k_>hQ=BEVc5M0s;C^XaAd$_)fdigv- zkc$}Cm&t58FQw(jgrn13@kBRN5=}iX@d@ybE1Wl|)${4|q_D(eu3BQJ4H4-57~Ajc z*{-3yHceIS9`BfGM`coRBSZC$vd~-^gac`HSGYYc_sd#5tXzO6Ahk+9Z3Rzb&S}`0MaG;b<`}^@UN(!O*owTXu861Zj)yD z@lYnWHsty$)y*4yAUxGa=WOo8Z@+3}TP4VDgUL{SG2qDOb-8nSC#<%EmYP|tEPf9h zXQu70;k}#+{c}39EwT)&92D>gMp?+aY zSe9m%n0uzvxTqKFCJ3-2p%KSYxk=CQF|wz=iOK^d>)YEHANkdQ=M2R&~mu7;83g*gJJKK`JH zJm)KF=cjvBr&*3U=fc>fscAiHXBN!RWgj(ZI8$v9`ov$_oG2#OYI##kx5?}Bl5k(| zpW1JFFrt~^G&nP8TR;RKTrt7tsQOlUYT#k0=GBAd6arVv1Qbb&BlCj+TDf7qMa3A+ zLtYAJ^2&YCc5^$oZrB@)7^6=;uSt4FyB+AdtUvtSMq6CT#4RO?MhYYvS>;=>=g?5y zaJef?*e*tN8K;lf%Mql}{l|Oej7a?7j)3~o@QZ5$nZq<&*oC&X!t>|7 zZgulOKClcFKbcqEfEVMijE3Z(#4~e>T5sd8j2u)B;w)*4b&{ELd0TH%>E=z^?3%<+ z)}y1{iQk{k&PB^17>V>die*G}Qmc($`WTYv$q3>mstfZa-9)=%^&9qCR-91E(~Bee zn`8KIFbPqHflR1@WAVpRwn2$PHxR6oKrH66GYhK^68cGL={F{(W@Eo*7utNJWhB=*9qok%Pkr*++mvvuxp1RYsu z0oK)UJ8wazYO4Uf>y3Sz_Um9XTBX;j$s4=toHe1HN;`&qU#8StpUaVni>yng9Lk=QcLKvA@cRm@4D+6Lv>CFKrr77o3q#3!M z-m?%j+V8#h1;FCsodV09_c;Vq`%AzqmR!i{wNMc*AXP>vI1H(gZ!-rds*oTACfJS4 zVp}n>Hp|!S#@byqA4BKFgyC(m;!I_{kjbf*#mN?01ogz#0BV~pSG`(r>D;&5D@@Pg*aL0mZr3XoL67Py_3)w|#*YuMMk0<@#~&^@ zvfxLVc)nd&4zoOJ~LxO2(`fw+JOHWGlY+kBboVKPiO!Y3I zwDENC<5K0_c56+svWXl}2a2<%pc5gYMCfcianF;#rmniaMt9W$-y$6!f3mHbZa(Z& z@+(o50`o)H%))ez%#)C}-fR^?M%)jq0lnr_S)mVt9fF7&MLmL|fXg%E+O(Zn=$MZZ z8z&~xG5V~TZ;GG$H}YUz-L!l|Cb*!npXF2J9Kg59d3@im#e=%oD54;}M$4$iNaz8p zpgJzLTcU>~{Xu5dnEBDm5gMOzr^{uWV@x*T4-|1pAyRagq)Xap-uxuMb{>SCahBPU zHy*Ml{QSM0yEO~fMEgK>2tf(RXqH$ug@B)A9M;`mPTqD9o@C%>J$*%Rs;ZEOJAR3u zgz<|^#%io9fBDhSQ7Ti<_$XGg>A5cQkLNmi+#+;7@7$YDs2t*Rh?PY3M6^JrK-5tZ zHmH_%H_|~qto%&p1a;)Hzl`5YeZgqU1;ebhI&7A&LOY4XlyEEUtwN5ux~j-&1WODx zMSgm)FJ2B)eU(PdusDIhLNjzaCvso0A@OeYG33+8g_j z^ld;Fp%0;RkC^L0w~go6jr;W@;&eJa{ifp^WSwa}U9FbVdUSiUv#(b7Gf?Mq0i7-K zdyGMQIcmxf)6?EUGu!NKnxW(bTLvfT87}6&A-!8j>FSr^&5;BpB@B*7@!=`+z4w7ks7h84NS6|B&SCl@lYSwY_Z(-NB^TZHouLX;2-@8AH85? zGdfUy1{CAZ$Om?1YWBE!?aPBJ2dGKy9v{{~sqp5}x2qZ|7#5VyYc=VbJ_n}oaVS~j zj@GQf)3-?hE7ExDVfsHw+-G&B%aUK+big&Dd;NMvk`L?LQi5Slf@6KT@9e~<>u)~2 zs;{D_GD)R0KMTqd$c7Vx^7G%e&*eK|madZBZ+JY$@h1DwhCR5xXpO>g%WT`?T&xeH zJj$|(@O$IM4#)gMf%G*s;CWfa}GM{3rZMdmt@ zOB!xcOh|q8TA6B7M9w@&I97X%;&?l%q0O0T!~7vj{&L^?&SnB$bmz3qBq@3lEpNdm zPH27Vc>taMe59>oVmnTL+*m)K%g|mo4j_|Ht48w5m0XnSY{Nm|`HRZjRKjw070Jh!>GsSa$5rb1*j`WG z?ouRcFMBD&zcAaofQQI#nzadaF0I92u7UbSlEXOXQiLjj-k8DAd@;6{;1oZ97>!QY zWoM5q4szXBv~ok6mWbVS!jQ=GB+8GZFza!T(+%#UBh5~^t$EO!@9Ey~ubXX|DtxEVEa zHbCJw=?=dMg#OC^c?Q)Z@5kJ8F|*5?;O`xKWazVC#g)O5=S-QW z*qx{IJB8A4coJXZAV-B@Bb_X3^yizRR>UO)yv(5HH|!*L1=qD_C?ytRo&8Q5NOmbx zXO+I*NTT@$Dt=sa8mJ8l`lu&3fYGVqhOo9}r9PjqEa03U*AW^CpHR zspoO1;M$-=Jd233i1#V+El8_?IaHX>rbCyT83&@86IYds#lp5hW}sT%kEbZ8o(Mrg zvK}!#IX0#*YaX$Nn+cEP>WG|Oc8%y2D;dvuNrhzewR(9a4IMczPm-T8@UTkU{xxA_ zv0AynG>AS2XKbYit8U~4#sD$pszkaX&OWlYP75z9EgOJM!P4=qwLbAgF6?fQ4APUd z#1*{|wX(}vjlUgG@RCC;J3ay`(33Vk2T9{4--#Bx(DM_W@vJP%zr}XWfVln3aHan} z|J%+(Vqn{$32R;0@;%9W7aL1ew!?Fiwt`)k@DR}Gz6yPRS0&%QZUzaz{_V2zz%M$A z1uf-oCF=qUl>~cu_1-@KS$mKo^>VEcBHNdpv$hCAYJQ({319qo7W z`_1W!z)#N&EFjzb{BH4spDabd9PTtd2?uju?PMSzdsUj(3gI``TF`F~P@X{|O5OuxEYs-;NxxWRm@yE0V zC!KS3r@&gumO^QI*DK|l>roBDD&B)N6E=CD$l7u&`Wha_X*H@6dxmdD^`q@nP#)65F`l* z0uq&sB+2RY2CwHi-}$P}k5g~eRdwHP_nK?Yp4~l0kM3=#hPnzd0UZH?AjGQI6mKI4 zCj4Ja1P=%P=dXMJ9)g(Gt18Ofbst{->7$`<{+VyDpkeAA{WUCN;(%2uOst#y0x|Il z;p}Ym=QEzSt9=zH>6liF{82t*BkeTcwKyt%r>d;$vqkrZ$1`z4!WQ=1E{Vgl3yCyt z*E8Z;s$VCj;?KP7;lDXdf`Li=Q+DH9Qjk*HVTO-3^W;1FJs%$#t++SQvIU0SCBEJF z#*d`8kqT@U7M9yDy*%(jLPB(rvt(psga~|OiXp40sK|rBry)#);MJVGqIBO@vuz@$yT_SZ+5?cz1dg)%coM@LQasqN3((qc(nw8zQq z?blzte3@2TiyCfv9Tyi=W<4msUco>2xhQq8q^R`l2zN)(XJbx+Z+#P=J)GoFgLfZ2 zdZhc=TYSlNKfJb}*RoLUmFM6#S%H(KC7zj?8KJK<=_?8o7ekWRsp(!BUcNok=dkR% zN5iyHVeX{2@7!7Ota+eOq8|{>GU6ZOUsqNkG{ow z##&#YYby?a@q$8Xf2l+ab#!3xw7j_ZoY(fBz**k5cEZx^Fe-aKO*Jb0eYrWz6!cE_;0|Jdl0>*deK zhbz;+zC3rUnJF#3a&Ry!)B0Y8sc|+1b#>Y0&rlH-df{@r^Igf(i-UFD->nL3s?I4@ zUAz_d!i5}RJ1S5u7q>y7KGToxVHrj&krdvE=)(A3fjeD;iB zwAy1MUuLmmk#o?3S@YSQqc8VkpH0#ydxZH(qF3U(coCbg_CQcAlm4NU#f@UYRn+kx zQW{!X{~tEmEkC!P*%?bRP%28dl!$4swpR9+UN%n^#7b+}FQcBBQ1b-dr5a zsIF#M_d8E&`-7myFe>kUx#>W*)~dVIwdR{kJLR8V4!(N*8b6~qBqoOJu8vNlRT&o+ zYI(SfgO@jGWVvO~2$grRx3xSn=~%h?{l2});oj!l{QR2ryGqCk;Ak{|!|Sg~Ji8Eu z`@8~IoRia`Y7{OZ)FQ=)iYg4dTWW(S_WAReH*cO57YpVZ*3iiuOpZuLLV+XtGzO93RXM)uLp0 z^(*{R4t`zQnF{0cPJVGwRld`W`dB@aJUKLhX3O1y&+dYZL2#GcC8DakQl;kHo}Qko zUxm8sOp7?&SiTn+MG@1BFw4j=NlQ!XLVz9Z6&%xXYv=lvl$>8=8Vdk3ym<8rE=^s$ zOLY38ex)OJUS6L2Tw1WhZq9L_+&1ysckfIF^9^=TsC{cOW#ytree8)+9^VS4JZ>6_ zAjUf_VlC`;rZ!XT=iZ6EWn)Y46Mvh#^N>#R*416Mg~i3k8#6b}mqG?bhx-DVq{;{{ zkKbFH%ZO_D@graV>qXUuNYp3#ix)3ec<&ZF*5`isP>)3_<)>rhLwVyBZ=i_uon2`o zzBtbaob}EGnz&X05f5^+=b|w&iA2WdGS<(0v>gems1So(KH00nq-EHhWM*MwLxjGE zk9I!^lGKC!{V}9g>#!We7<6lW4@-+moMzS4)YYf{&iC!CckpF)l5LN9@%e5fj8BAK zO7?4Z53|aidv^*i(1UDto!jJax{JFQ;Yib>jZGy zng#}W472HK=aY9P)f2D!(CHKy25x@y`0eBMw^!xyR1?1U!CGtIOtrBYj^9JAB(wO< z?W3E&30|apt>gE*nIMu@UQX?`wIpY6&zGH(1IrPE=bE)fPVUa8wDxAGXzJ($!zM{T zxvjKlKE?2?VJxU#Cm8cuk$#uGJ!H#ILuC2rQGHX>8ALXPCdvevpP!#wUe?m4$9Y;` zFnA|AIs3rP^EL}&+#?i(iD0AiN9D(lRLU7-;^N}&q_Qq9!r9r`4u9t~X^3!^etx1R z3C4n#us>Jz@FBgr!wlfLy0=)sDlSfo_MBEb1qB5x99-NY*yAIaD9HVH?({toq>N>g z+x}@yvo=~2+hGuk`Mib+=P5>%)H>e6!h$hBP0z>(A(EAo(+Ed0xk9(rK*%RsFxWWh zkcvJ(fXJ5z@&*RyZA9qD4)zB%V_*8e&1G8(>(_>W_u{A_W(LclQnq z2rv6=%x;eE-b+jtc*}l~Rq6Q|WT@Ih2;rC=p6N_@nX*~4J=O^S&xD`mea+R!$xCB3 z^qrlZ=xf*1)5Aw99DfrGkBqEAWMLu z@wlR;HSOj#KWlG>M_=%--B9;ap6=`qDY~PhlNo+~jV?3!{vD$fX5VfBk+kaMZ{Kbs zvQADLr74$$gvd#P&Fal?ky3p8xyKRe`@(}eV&C4LX)7jrTAzAw2Y_O1%n%H3Ggj>} zTzRpj3(Q*RwQW}oSDuxf{REDE-@ZoJb}3{->nmsN-ax^NK`>=bHPh?YuYGIcUT{($ zzwojZ`gvwBjSGk~N zbEwbXx!y;p{K`t&nwpxnFJCl$FGFsTbX}7Dxy3Y=uRfhQnECYS(?0h6596)WU7Q6L z?+4jC1bL{!1XFEA=tbhCAW@gYvlQ?d$lItQ&iNV z>sdMk-@JLFVPFt3GxHEOK@-vPNG9jYgQ~@vvjw-RA0k;JM+!^826lIMv-0wq$7;OR zAX%rqd#AK(xRZaa?^4USi+}-#<}bu=;3^W_=>PMR8~5xRchWxxKQfO{;I0O!mB{mw zg=VcJxw=J|Yinyeo%X(1@J-mv*k&1oP#VIN3(& z_WSbk?R>(@#z|Qe>bP`UB|Pc8q^t4vpPy(KhrCqivGEXcCx+j;1(k&BOd{@s6>EvT z$=U|XE0~ClNP-~suV247#=9Q~re$YawN%O_KE2bb4w2=%nPW6r=Z^)KFYoBM9xl_E zRM4FwZ)`=zuXX3nbH}yrFJI&g41O9o)hC&$R40Eh%d|SgL7M{{!qVQpHNpOKD1_!u z;aBf6GslHz*T|5-o438CuD^O~qWAshz^2Y+oz?ElZc}4t3hTlA=3l?qz!pK5lHD68Bq*L%JfA!2p z@TQRTbQUxx5<40g%ql2QDX+v%VDusCh`ao>*Rm;?$#-t>^2Qf0Kh&KHq8qW#`FXwg z)W58)>Bc4o9J_4N@kC;TM?_4{XQs3OPMVaDKpKpYsoflpiT`+p_m%ZK-)E@m2_Fk>W1u}qx zcpr=%`>h=b7mh4LhTWM-uumPZdZ}6k$qJUNaG1nga+L&f5-M{zf6LO+($DpF%YD=y zHDs&{oSgWKQMh_~dTn37HudzJW%OQ^d%(x-2Mpttk?*{d`>i;09@Xp0zEvcRj8W^0 zdYm61ZPryXu3+nC_Dup1)!C!=?=KCP5i)tNV(=PN1+1?->eqNucD&+ir8e^Rhd=(_ zEGsQdt*D?;iDUo*D(mQ2yzVI|B(w(kZ93WOkAJVFBDfQp1B8a+-@SV`H7g5GOH0er z%1YkaI;VSf__C_JvMI~ASaN=m$9Hx&OCQ*i8J9`&i-?@mLj{A=5c%$V^OM;91%dZ!~PsULDaql zb!>l38T6##k=ZQpLAVfQCnupeKBGu*$neO>6Zr0p@(u9W-L;#`0~Piqnus+ALLkpr zXx80*ghg^*;!K3{)81aq>WwZ*AfU~qPS(!O6maK#hGzHg)5EQQm#M~I`Ml4oo-7r* zI9QMYcc7%Cgf0(rwYmR#N)r$e@QaIo`0C;M>HMWzqP!A2?U;U4@A||8-k!mSs%j-a z?3n%%y~Br3I(jb?CEZAF-n?1izQ%&yH3Xzw&Hz{WM<}MEfSm#VpN1%_4woKucmz&4YIF2q{7C=hPwzDmwT=0fk`fU zNh9u^j=UcPo8p-+Qq$e-?U0{}9o349i&vYejX27?dDa&s_iAieyOJWAwa;J+N*&#Py*AVl}?tQzWYZ;l2A}k z)LE5H*?2OgGX_s|xILLIQ$jjr-N5Ml4=VZW~Xz7Mdc$qCD%%CkrGSIvdtzP_h$wyB0d zLbU&znXy(?RlRWPfERy#Ed*Ip$Iu8q%r~ly1c1`7^*vv1_fy`>OA?aCq_c#;l`8>L zQ>KYxk1=2I0e@Ggqamu)A@>av3b>BvHE^M*RHzeh|*n?Jv38l~m%)hn8L8KWAs>iS}Ll2`DQ1 z`|p4OJn0Kh98kLbN!{u~&X z&3=th5F50S_x6^8Q2e|zMF^D{6mpYrP#QWqc_SkxyU_{^y|~_i;qWE%vF)|KTQW)w z@$}@|Ew|_;nS6KB#?34o9nY1sJsJ))qS^iPXCieU=idGMkC%o7&=taLSCWE`&R}U# z-_Q`=^DhCvemy|n!Cdh1v&o{z&T!Vnf?8!qM?naOmX;QDLdpOB@P&85Iqe+E6rU~H zD|EzCjNPS0PJbIwO2;AGY)%K#h~3(46(^6}bczwK4$b_*#wCY|l-jGoVLv}+`AFxi zJ4SzEQ}PIZoYqZNRzu9D`b2=6yz<6ves41d>3mJyWh~A8G0Q7-FXq3!u$cL&&^YH* znX$L|%kp_N`$c12#^BE1p$k{(=pvLov`Lt=K|Jw&zm=$QzW|2{`88;4B1HaAQloh1 z9m!yIh3;UdRVE0a>*#PF5GM5?*>coW@b&7SPb=w=M}hKaT>Y+j@1Df+G&kfd)Lvgo z#Tzyj76S4V8Jr5wO)Fqd7cO1GwXa?aT3BWUN1Gp0hj=PllxS+* z&RxKPLK8AP=0Ya93oI<@%v4B3M1=d{=AcQ*kDjry{3|>hTwL;+nqh5E;U7|&(KZDB0JPDCP)UhvvlzVgz3f8gYKpyBNZ)t`x*nZ13vMwV{1)^Xx z@{xE2Vk(hdJH%k7O@^iNdecTk$dh{rS_lcGHmm~zs&V&jz-TeSiwEW$wMKe+p^1qM zVhkiwyT6|AH1qlTLv44a(9&xnbM zh1iaKG=b_#%xCWkkoL6(lrOe@%_h~D@4oHQj}qMFkjE>rge*5!G?KBiK!#f}q2BQILmrWTt?b>JY*+Z_7S)0R#o8%Y~wRtb*1 z)Oz6`z(_@&GX)!i)O(9li3#)G(*jckxQmGi%ibA3cKdoba($4dKlYDqV-6=aYW}>y z-Or6eqzoy(4IHoXhdC3R7hxp)T)2Y6;YyUiefOH1jyp)Rng$3WyWPVNTtDb4h_wB8 zkr3jG-^fpt-4E{(#LvWofs^hk4v4w8wJ?mGJa%Y6%)Mrd?RTw|ukYQu0l32L8yLta z3D!9>vY2NCe)W%6T1P5mKm&$9A$oBFJW;In`1d2>uh9!uo5}jUNgT zga!v(`nEYn9X~U?lzp#HL?Jz4W62IAi6w?uRe_E(IV+TGlBvT<(Y-z)Tx5PoLKlYL|% zXH8^XPoC1mKnjJ>Tj3%^PCqfhK#<0Dv6C}daU&8*tVrF-QdV3@lSuvP3(_$WV+-`z zw5X8NPa6JjpNz7{!`1__k#c-?I4!LB-#gG}Ku$N2jhvOnI5o%reG__HS8j zJ|vsf^A};2I~dF#@w>jS3!rbyiU9FDwN^1Q=Nx&u{4l})9^t!YMX5O5~MPa5OY>neVZ5kHBWzrCxDzOHu&Vi%0z zL5+4bxUJK}6(Xl+02@pYV_A`(dZHFK5Y`}T@5HEoD}k3VGhsnv;zRB~d&0|Dh~m>H zapb`3wzKD(BderOeeUD`&z;(ltHC`33;J)1|KnlRCUyF@1n6TmVandz(?7W$9^`K5 ziECo}5fDcLptH$ln4qn!i)01ybU+==qSAqO$s$C9G9e7Ts3}IO#K}d%S1A517kF}C zR0vnozwdIM>;qQ6W<&n<-)FB)JQDvcAd=KYc4`S&By@;9{@>?<|1Es>#6w`g6K{nL z2M*Dm?&|D`AqkO}rf5Hd&B*_EkZ1oLqzLV}u;B|Qj*DL8-wrOE1QoX5bpdoV!0xj8 z#2e5)3@@<#?*;#JCY)CD)A%Grw8;LsBrMkWZ?Sm;WKitiW(MXN=}zJjjzj$aw23*| zM^0`Q+waYPKH|ZTZZi43;s3Xjkbf4CN8hr^E#%~ATq3V|65X`xqRgQ__E1gDG*CMbz zH8r)>#Ej?^FA5AdgwbnB01f6T-s||T%fo>gOh??=-|g$5(hUXu23^F0)VROBf?nKp zX$S-1f2~Ht$f$g8_$$6gtFFyWF551X+^+Aqhri5+qUAZo#Xauz-lA9{l$ouV2*X-+ zyXs@2&%i)}tItb~Oe`4PBg^bf|MZDoCxJKhm)A>l3n7&JlEdMi3)`hj8Bm|6`0jC{ zd8fE|D7XW#Unq4j5GYglrKEBXP`QZ}ujy8-zEHk>E(+@Ii@H86xANDWH{X=QUFzi10BPXDOj<+K8`p}Z4q-A(@ z0nV zhyT*s_fMxl3fan?`V`yXY14l>`s{znM)B4ueqsByJ4>kii*>7N!aCb0Ks+G3yO2Ik zFh0pD5RDc8s3;Karmz{(%<{(ywfD zw&`DrGXMWeQDiv3{+Y?#hL`l;eOj?fU^za8+k~qTg!;pqNEV&U`yV>!2Z>zp|92$z zVxEjv;r9QsJ1~?JBm~=${o^?3T>kH`0~IY~BZB@tNmsmd8F4KA_jRpDPM%Cy;O`s8 zfX9SLnZu`=ku!_r)Bj~~XHsHTorEVl?o=2j1{7AL;hLy8kfv9EEqyL{-}miYtK zXE@Zs!Mo}^*W%18T3~_g%26q1Uv&=Q8&19MH7fX8vG%RYwRS#unYqh58`$Kec-RIX zwVw)Odx#D8=J9y8)h;SFQKLH!L&n!sfbH@<^qpg*Yk9^a&89jP_?7hjbwbS~GubF> z3hjM_G$blq?={EZB9wO-W)5eFm(NRaQ1m}3$pnFe3aJZLP`pxEm(;AjpBAu2rDNL^!~hxg2M#)l{jOW%unv{{^ATVLlh zGT441Fu2X)rt>!j`rzM-Z&!~$Xs}LU3jQ%!cY}Vg2JfHBI6J5o0n1ct}C zEshuyGzoRRA9LW3X%8Rqj*~Izsnlo*;I@tym(k^RbO(OOB!z?T>D!4(o9V{9o2_4~ zhxa3XE!GB5psuxiDF9nc@4Do8IB-k^div}-CRPpO12O@|vL@3r-YIfOp)Us+Z`F~N;t4H z2d?_*ke+Xa^GMYUw>?$F;O;Mf5trIHe^(Igie?T0X2RbI5rLw3&%MHTr2#qjm>{`B z;RRW4Vj^Ct^PB!$?77EN_qIzl)H0pUAQhjcq!f2n`Ulk2)JBhn9w~U?j@b*E-0Hm7 zPer*Z+nY|i0;e#9Q%r9%I!h$MeaGPKK+G`S5W<@u6cGFL}ZSMd#U1M7aM0bpG~VJO}O`mE(iJ? zN#odVoGpY<1i{|Q9dGv>1eph6tgy+WRCvDYkvD-K;e^jb=WPZZtLu$e99)ax>A23Z zI-nvdKyi(bw?D^;NbJFp>-QonS0dxr>a9*ZUqS~YKG4W8)wT)#Kr84D&7Yj zJ?xuZ&*#yV>a@A(FM8lsy1bCsx(dofs|NGs3JvPCT2Ls&XDi6MiF3Oi((uoqt$D?8 zQU0dMRR#YIsCuA0DaUVFN~`@5e;zA~|H1m>J|e{(`_J9RMoS+jBc2lv-P3;7%5~ji zqU{o0=W@@ux0zO)dFj@c5K&efrmPwd5%%6`>Q~x)YWA^b+y=4^h-%WCRDB8LpSgnP;1EhX#)2 zTfB9ZQLQRRqX1-I_WLVOTC(w^ger20TS}%UI3;?FdrvKM3dDsp6iAYq^w2HKV0+ho!iJblJ_kj?5D^@inb)I0aggf%9)DDda@vl zfqO_u=)1o8rmcnRV+2(S+dU3yK^RJNEBY#KvUYdIKXi%{$@-Yck{wC9lnHU5j#K#J z9V$AX3@(VS-I3yACUB|y^h%MxWr6aOC{bIIhSogPv`GX;z~S$7@0vwm_A{i?_UPKk-)kh`!?X1DxoL2pu%(d*Wj3xY^N2~ zk^mPC+yg&R);gx_-6sGYG)^UOOEBPYjywqi1~Clu1SFEW{BS}avR=LZAQ58>r zA08=q;}X4y9s9d*hTTAxHvbF?P>@#$R2}#28 zxY5QvEQytW0)+yWa_K*^-U%6}N*wQorPU3UyemCBuNt~j_~GJPGpzs&55^DG;I!m} zRTzG0bt{Ta7Im??MEw*qxWa!$2Mv%+3!a%qKmVnJ*#b8@`(RAHLY4C2bj-W6G=;U5 z!oSSQx_`HX%;HF56(g%-=S(>q_5y8m8C9OKS21Eeuol*Rl|mm2sj17wxz*XC?!#oQ zM*h%y^-&gzor9`@#L$cvc!x<x>ibJV|ia=`!j!G-4odKS!?8FzVL5uViZ*-aVu512A_vlji^x&tgj$v?-uU z#0PsL`rkfCmf=C{bOKo@vbK`2l=v^j;;M)#pq$Q&qD@#8G4Rr3`yur->p93**p9Px zZRK2imuw(VS{DArS-zmHIkn$qWq#;1)PE`!GpFW9@`khp|vS}OCwcxS?Qh=eNp3h zsP#{>HJ&x~qVKd03Twq7u*^(}HX*?K;&LwwfyrYEQ6;|U1=rT${3<&5*o zqY-l=|K(^UiAyjh>j$(cu$?-3Wb=6-ZRNIoQ`y|FYlc@fw0+wk;8^hj`Q`UPm_LRkO9QLssMlh4 zt@wf=+|a49LtzAa>o7R=OqAhmRVmcvwM>4iKQ4AA!>UPgI$+M9!96&4@v|Z>>H8`{Th+QA;r*f^DF>g2GpN{yF}Qh6&ZV}^0opZrE2dVnfX?4c- zbwS&Xj;A5e2Y&}&)ga(5%~el+bFRE_!eV-}1?od=i{ z5Y~oj9cqkAae&J)Z(iiu#GhM)@`=Vxy0~mCLIK=E>Cov|aE zytb?9Y+Xu!?u^hFLb1T|mpaVsB|6bvy~SfXD=Qp;sYJuyLo5W8()9QDjU<*DA+&R9 z>oE!^9Uqv(T;%U4Qj>q6@x_`fa1Zb~9`CRdDnKg4_a+Y{oS+e9C=7-J>H;y$E`w!3 zX6t2F`qgdmwOz5&kcoV<_J=-j%H@`lp*tm1WlGqI)l~f2W}HBd{(#J=hwO+K0S5;} zq1{NUlXb(PNUL=^Uqp7|m2r!#LLux8sPxNLxqGRV#K9vx?s>lC3jV(54sV%)1JbXx z@!m!|CLWtS8kn08@-k#ya1!bE!kS0A+@AGAJh~lil%Ms=Tyy1!rmx$14^|xz4~i!| z>}*-00S%rCIl2kfsM96ICOHvyd92pMe*@(OOA^umlPsb-N?7zcn^reFm^bt5w?Z0Y z4mcn%Idtw`MVXSN)#-k&;ph~Ti*E&t_+EL?9d2Ze9mH726M`w&iOMhwy0`n!Tq>m?3LS9MbU>mdA1iZs#dpoU7T}l(#ASJj+>7oz~ z<6PgFp_DGaANPpA|DMg}g2%7@VApj%k{ou`+FS|7!~R^6ro8F&>I8c~>=g~qGt>hJ zXk+T zM1PMF6noT;V`C5uhhvaOtkSS!l_e-oo6AE=~MaUFgY^j@1p8eF+}Ui&5BmB{yOK|oB%QoMh~8zi*#5XDNUTj6<%oYo%=epaO5Bg?MAWkSYKIejGw z=8_2&c;(h*_~gf=hB@FR8bP_xz)z(XXP#IwV_SRr(sib!%}J|MpKUWp0&3db- zU_1h*@JD*J$Zdb3Y`ghj@$#2;S*Xf`j%Hq?X|nuP*m%=Wz_@!J_l&&1n=8%>HhgOW zQ&e!RY#t96(d!EakL+@IKsA`SZclQ84Fc~K{L$bms({+)etiBZ>jkEq81}Jo84c)1 z=I8Sgk&<%P#(}u4!sv14@bJ*m#wLJGRky}d7(LAM`;`$3jM#y`4JtL_2jeeSu3Twq z!l)jyeG2n4P-U#9jgSjHU(FhMT7B_RG@I&=$(5%wB4eh>tq_kK+D4#pfaJ2a-Wu$_ zI=iNqgAM$ly0nN2_{P_~NZ=c!?KS(d{C`Djxm|HVW4J({qlvj!C~IVTL}WZaaiKAyt`1u%;T z6G}tn_DtxBw1ovLn9-hwSvaxN!MSu8!-Jvm#`(TX=*gcI%0Za`lk!&Noh!GSnpRl% z0pf4Dyga*XE~7)0His?|RzzRR3r@DmKj!WbG~AcklJ{r%>(+K~xiy=qpAqNpE*G>f z9>X*lyl8yWOC$lUmCvoL1RE9vH%=ZVzB98i9jnzzp&@pDa1-T@ zeILn5+3hvCL+oO$`f@Q}z8p~!7$r4+=yg+$kuHB5dwIW~w-W~eIe_5H{&Sdzhnc^4 zca)*DGeg0EeZ#HqW5LIdtil!s@5H9dfty|V;c0T-rL|v|cx#tEU$Y*P-xUD?jPj9k z7Kh%BYf(Sn=Ux*M5_%a{>-czkk~sd&rI3fzwO;38Q@zu4#KTVLYH??0C-uCjYOKcR z(z8kGm*|3(Ym{Wet5woKt-j~AuW8cO6hSZgq*#JF>G&&&O3x>O>#;Dy@hi0^&cTv~ zQQMVd$yov%4=Yy#~ZeWQW9#D-Wi7Eav|oQhesh6>-7sF>>m$(Rz`nE!?`e$w)j)Q#_M;KF#0<;us&mI!+Id5Ho{R)2jc^rvRq z5Q1j{2614LaBgw&Y3IX67&XU+PCB#-YQ3H43|xcPTOOL5o4C8vz|VRFTr_NXMXw9R zrLqZ~pYgo}!$4U3@0*)1@bdCr&TU@S2UCy=7Dk+mF!aumbe~JMNBW$hld6~PAU9&HAjs(XdQ7W}jcgnze1S~_C)@*=3*8kQUiJFOAnQa6sYx@&gJ zdGK?qnyTCQhnmJ&WkSq^e$1bSY!yzKBH5=W$#i=O%W=hgBWL=44kwPan)?%=i9{OS z@rMWiKjQLmfv`uL z>-`WfeE7(&=|KO-y_J>=ZXkkr5ffqYdYhAlpo@{~J5khr69#9-#CC;2$&sqHb5W#R zAUecO8Eybc^DpcP|5aRZ@|hwm=!IwIQn?Kv>Cc1Nr3BZa6;aFAVaPV?5AJYEzIRKs+1|+bX#4EZ1vbVXA1I-Z{)X)MP=hQoFYS6o0#5OmDaDrv!24-;&IxB7AOj4j}KsG-DUikdNHu@wus zY100QFVku|w`89xvJla9JY}T;A_A?It3EYDD~I~Hji{JHE^mm=>m2+&W-gFz6i{S} z&NzQva*L5vY$J*%WCUN~CvdznZkll6>`|Ho zwR-mFRrh5djLZFt@G~S(@2@U|zAcitLf4lBRe@qp)44V%6%WOw&EyErL7(1|e2Bcx z=(8)m*$$te!KqB`o;^$xt&zIpto}TY8r{38BWrrFvi%6X8zsUA91b@gO}bG>O<;$f z4j*L?OLL!npttowXaeM5eo2Pw^|CS3CE7P+<^9t&NVjy~1x5|%l0Ybi@ZVAJCp6Ft z>oMYZqPRpNI9#+!b+*Of%k<%sVPlOD2uwb_?Mv&NBar)J)+a=`ZZGd zByp{-lq;a1t|?s=R}a5Qg-{fSHVw%+;|_-dTw%GhTEQHhd}0b@or-qyU>h3#AYz zbcohrlAAEoLTz58r3=+7tqtOHg(#^;$zJ}Gq_o;O_6EPAe%fek*%yiKLV=jVZ+g&L z@&OHwK23?4Y$YLc1FBk9aYff>cUl46;b78w$yuA6H^Bdw8VhKMnN@-@6{2|s%9sg3 z1Fos*=`{xvcX?C^t&7(XxiLl)b{ez=Pum;6Dv)2k^W_5sl6JFe(~pAmcs+1ouwPA;x~*ke<|DtZ~)>wBKusPvf8h<#Oq z*<5!sCok_*i$70CoBBZN6@}o32}y1UM=4wZvot3e^f4XY=Uke4q6kUQgVftbM;Jm**S5#UP8S~GYDMgB)B{QV3 zB>M?2n{v}_bit*mn9-XGHo5uH@L*$b*7$3uVZSn4eI0-5F`H@wwEW`Flcapaf8X{h zZ@}0>3wun`jkbVMA~MhXox5jQ3@*De$Xu<^Q--CSWB!M4Y0LY^d>m$U@sh+6B$}h| z-Ce11dWF&S=X-_~T@IvWi1a>re~LrX6O6h3wSRsE*c%Ybk2*Ta{nlTgwBg5z=Yi&F44Uf3uXiX6E5irzl)fDZi45F}SDs^2)Rzq3fuxoLI41eAU# zbt{H!6E+v2mkT$@%0+RiK@eee$)UG;aOord43|LF2`Gag)+Mf;w}o*LjX#{E`V`O2 zY4d6hHKoyI?`m^>iaH8NT2mlqvq*h)bf?X0UKfZlQ|db15zi}et=V{o=Zo|cwioG# zC0Bi)eUvx~^K->mUda=IpPUVzVS)4yKpH>Qb}Wm%KjJ8S-^eTpr6#*hZ|hAz6j4Rcd6NnGUyi%3v4 zM9N>Q+i-F`!?^rYmHq=idPUKuI15#Z zj&?ZcGvZGbH(t{b@O&Xy$>S`l>g*LH5*tvPtbL7crYYN{{kbvM{VJ>#H0{+Bxs`(| z?*zG`FTV@K7#SP1V)BE61j^wCU;4^U@vpW|GId|RLUS0XGrD$NK$|2|`bD{L6mT@N zp_dbwR>v_YO_6kpeoAt$UW7QE|A{d|oWJIIOECbmxkS?y=WjOWSDmqxN`UTCH~O$< zd1(l6Ac#0Nxq=ilKlk)|Y0)u^alKd&dZJN4ft<$9ZN1{@AR{MuU?IzQDn~%p%DqJ$ z1-ec@EK+u&UmtenVKAL)+6w-?*8?yz&?t&m(hT+TYh9KL6dV#*+0sPr7PB%&I%oqZ zry0V81G;1~&)upVCBjxNQlG^GK_cBMtr6Ie;#~JU?ftvaZLdNvj;6`>(rPT5$QCUZ z$mFO5VKm;#So`?Pu6Q#&t%lWdJUFdx?xF;iHjQA$L-bo&du7?q%CQ)+$2kOJ4t&1p zpsL+cYKuD5(}O|7pc0K^IgiI}EeilbFjFuAtpJl_3ew$7FAivz!sFy*dxQ_7Ew2KE zPgdaN_#{jKEp6YQSZRM^QnyOA0IKR$8!P-Dx`&+~lp#e`mp+!>VA5ne;kyl`=ZKWy zW(mJQ_o464)qo6nZ8J1~L2G#XH{W9$C=Su`dk1>*%cDMr4-8(F`O}F766gra3d{ZgO?8u|-`E^+G+!4eQCZ(ECI$4CTpA zkRm=APIwSn@|UkB(7zK+UHDKY?6q!Fmw%dXOV)S(i9f>Jha|^8KUi44FsVCBmR3uW zDU=0K@xKbI2+KXM=k`wetrOSohC?Ibf+6i$51T9T z0~-FFz$&~$pPo^wt(4hrYBjOZhxOK*2@qZ9be1Bs@X*$bfq@9$Cn|>pt>hPGKD<0;Cq*CK5S4`S)F>;M^{*&X`ACGQx^D!;NOyr52S1&n0*ITk zc)Z9YSWB=7m^a7O5yr(FTCy_|eU$NEgLU-^^BgD4t%cF8sMgaqrw|3rMMauIHR?*U zm)Nop&;h}OjZAwRzthhxXh3xjzuGptN>q{NP77LtEFXuoLWbsmEzdmym!d`^wx|J6 zTcJxA;Qj!*Lg)pQZbMp{#KUg!q$z}fMi3vV&mVodlOQBf&KU84y{+E=E{e9H^=Z~IBWIaYZh0?Tc5*@*>F8`PtXOdY3(2T4u=d&dq`W`^%E!2s(D|dUEs!NE4lfE&o z-}F5L)>ti~BL;uGQtI|^@`FC^``E7$UE$5YX${d(B*iJ+0doLznsb&Ys;Ls3%h~vo zHW^QV_0U=vtqH1ziD!)mCt*;l|E&oDCEXj{ssq`72j)P$*(lP07Jriv2kS=1=7wj1 zh(88a z(RhR8?Eq#d8$hs!{$?#HSJfy~@GtsCds?f|m#&T{fu!Ga@nW%bF4KugOiNOl^^GAq zY@j?+#ioNb)OJ9SpQB+2ZD;QFmZu~Lf7#{N!n5M^8C3lXyS^tQ4@Dt=M5t8J-4goa(cI-JXhwt^I@R8quZA^*Vlm=r!WdvhK0e>-J z{_b;x7TKO8XkH}+8_)xVXSvRuD9C5v<3X}RFgsj}f!^aD?fG6t9EzmlC+Ck6<|{CO z4=4p30R@lPE!yk)aRV4PBx`0!H7}~k3H_ARwpvD)LPjR};+xtzb<^rBNwJ>UqPkt2 zga7VtRf0S40_C!z$tmWUF6)lMLg3BH0T_c6zlx9$ZTPDq3byqV(8b}Vpvg0lRgEo7 zdy5AuXM==T`zMWY7+`>=oL1BT zGy~8{?*_d!Yd(I&J*5*~>kB}g@dmt)Zdme#hH)yNSntQ14j;8M0*)Iz3?0yW@btW$ z&Lxl9*!^D!VIot)Bf}xu-mk~3Q#qaE)6oKNPw)E~_pQOtFgL?CVM4>@C;?z02wEF^ z{OHlH({YBEW|S~m0$dNQn|?CapgbhHVTv&^O>ip$I18^>Jsx&@aX9)HI&a!gL{Q2= zRFL&*Z|SR)XG>zi6flS6!Q))@+KJO~8rC)#A%q@y)v%EjZ$)+6qm@QVBeYD@v@g^S9 z!vvhlx_XR!RU;J|zhAh!pb~&;Bq}TAMgWGHxto#^{F5&KN@`eT?UN8S-_{?f%S>UT@i7DTx+j*y^w@%$_r;yEZRE^-Mk~P3E|Z6gttqKdl&} z`QdYi2!O(p+$9Ki81QOkj!8jlH8@tRvDaW~NA=uCt+;82go`YE5)WJbxElP34lf@x zY%HWrqpL(1_3)5w$SNzBu^Bm2g2bDcLYr834B~~ngDi{^vNMK9i=~sti|vcCiuZO_ z)LD!vI!s_pHm$Z9{-vV2kqC<-z&Ztg6E+E~pbQi)p`T>P`6G3GvyzlRf(OtoDCPb3 zW+Y@ag{C?NBFpL3+olI-!f}u)CAi0@SaLGxMT22B0L>vA_7>2mw15Aui%*C^pLVz*(?#&Hz-egd@U7U;CD0YeaGBcDE+m8iY$wrg8?ExBQFRQ@h@$s z%kq_iF>QHC9m^(B$u%&Bk`4U*V!~4N1e2e%~d;#p6AC6&BF&vO~cXm$h8 zeO-3V=$j{+e`uJ^i_q*5CPA+c{Tp}Ec4|g~B}O_D>sv^~jO%QzB^MRnU-8r%*TWa^ zYhZRu0d;|kX8h4#KvN%QXg*(X&Cj3**)KXmYk&#?xzpElx6K<};Sr9BJDdzxU$CJr z4klzR*+}8z4znadPej#&;sgVpac072_VgjvQ9ec&Nw0-O`EWG)W%HmnG#+?uiJ>Ce zBK%|`o9-iwI0OPuYw+v+PA(g8^v0%UG(soE)+R%Zy zzifGL>3U+qFBMbu3tB(qBx!n#vYIUR28IK8Op~C}wxQbwsd@EV3yg9paJ+zdKOMAT zeg6lNzpYq=Pc9Hulw@N*3}&?oYq2E4I?Bo>@)P~rT13Za@8d)DF!*U&-Px!CjX(@7 zEd>3uNN`4|HHgjy{vhip!w$w&sJXe?Z;Q&GR6^wrg>ooc5fzDQBF$K$2z8@{vQ|>E z6cu8KiKtL1DoYBbg;2&e^Sm$hdw#!j?sI7pJMwR(KSrkDakjsT^l1W-Pnl_6-nRpvY(ThBRmE0*wvzZ8&Gxnx z4Ng;boW^XuX$AXJMmc}f7*INUJd8}yf@u}l36W=#<(9-jA+tMf{(APK5Ai040Y0BDQWE%65J)$H$3$VvnUOvu;q)ypj4-$o1;?lGV zQT~TJ$y)5KjYPUZ%UpaHF@mGF{D>*e?rA}jGt?}uw(`mDw?!Axj1di~ED3wA{cxUjyxo0hNu0a`ql-gtlTDIDxP6yO)(S`eihA9f)cvws&eUr&DIYzSpdMyhOs1_v(^$Kk8rssrXgwu) z!$l!tf^T6NtCl-M zd>iUdr|90wesHQ(@ZMTavd>L!bJr#VH;k=~udnbyX{(H)xueMDcdVCJgn!Y0=)FBs z<8@ZfrzK60W3fCFIa{;&FUYjd@}3+3L6pJd*`-=xx`z6gGQBAN|ji&Pp2?|;aq%GY)TC%%^$&t$XysU-ehclhSuzcQo{kUZ6 zL*g;PP4g<5*0wNF%Se01?5Kp*^Ajx9lyVL#E>k)3&?=Qh1h@}^jtF9oUkgxu4MmO2 zsG@KFoV0Q&-534LDdwpUnAXI`E&Elmj9ooH1oRb%+rYq@XkBy*G|Glxw-MGxjZVEvOhVJS1H9MvAPaawh_xxHQMe9lxw;b? z6qIBy#*dzLe?>1P%_5xDBVVmjb6v`dm-Y#4)REm>!rm+9=wOo?#?`a)yz2M)cR)cp zlXoEBktDzrnpn2uv+L;--50kRE_EtJ!obPHxbP4R8aj3`w^=k(ys|t5?`SgU+IGeM zN7376NGRh!|KL1&Jw`W{^vriDKe2?;Nkp^3^07aiuD?^1{9It=oatw>;+(td;{ijX zJ@;R-`Sp&2#Dv0J5AzHz+7htB?osC8XEZ;+;MG`l%wKXx+u1}C4VmxWtqJGW@J_A^ zqunk{L}z)9hLh;+%8IC$7DS02)Vq8+Hk*fzA5_C>{BHS|_!Mu!yvIuR{yQnXCZqB3 zYZ)tlt{cb49j%4bU<77I#&Ik^O;p&9e+iTI6);8;dM%?s*-Y&fNk~RaAJo+QR;t{( zLJ!?7r1qx${fahpMFq`Gs%wLzl=S)47l=q(3mD&dS>h{1J||J^;hyruD@BJVWxL^j z&&O=+H$WLe%VAPn9nMvrC#4Zilk!Q!AgN&4xqghL6^Ec&{s4Qp}D5;=p=u7r>JPr*Dm-wX)@U(uJ7-#uyC0_2YDDFpj)R zvU6~Va^eo014K)Jvaz%4JjWVe2y{?UiT4(?^B7NNMMY3|M=d_}`6rWuF?>a@<2~OQ zCGQy&gKAA29FEPSYWsCJ2^X|q^ddDBsC&>6!Dk}fkm4pawWCjRbVkaNZW+=bx@*@i za|?%%I3>%)Qey;+Xl`y^p0mBAziF$J3;JUTyzLu{cZ|?i+;7VOpQL<>B_;4F3mN@{ z`Z+;C&pSJ3Nl8^tYc_ioZkng_S5!igVD^s0sICh#oQOCN`uI^td$o;DA-jrgE}KuI zJKM_}j?_u`2W^#}B~S~>6(knbGjBv+tJUtfRv7B`Vy3#islAjnt{^P;Kd?-o}oAr%Js zrzpj_%syp(H$qiMII_{9oTRwnFhUuyjCA(w;l9u1htLLDo+)>!Sqd#c84e!1S>a4k zBBG)+YAB%ZP)5#;_H;#;70`NEZ#iFoJ0-+niiln*Pw!Ig*}G_elP=SG%1iEI9pBhm z3MbsSzCn5VLE(Uk5!4pYBT#$rPb9WOlk-c;&awkU}{T z49-;0aO8v1g>+Y}cob?Zp7D)u{~j&IYOGtdL^nKpMw^s6(IKBxM2j;}a36bnAN2QY zVsirvHJ+yg0*bOaRim3eFY$V5X=|qBb+mh5P?vz`P;<-RX2ZbnnB|gk$$?=j|5*aV zHu5FIcdj=XYWAhP+O1R5xJboGRsQRUU(N}3uwK1~{#2DaJ;g2Edf#_@_NH72IFYHN-o>VdT*J^63Oxg{@Ltz)U0!vw81%;E)+uJf5jWW=BeJ&IR?o2U01d)w^1| z-XJtPTkZUlvVF-mSn(-bAEU~|WqbCw2Mwp)D3sxSYGRT9=N;(UkaA2g(hwWJ5pSfP zP*@}61BB2T4sg-H_SnCF<2Jjj`^hU`YeMI&F4b-UWv0>25{#p-DBNU<#|OHHM+ zva&#HBSb)m1JM9ZOaw-T129f3XfNK{9*ahDh_+mTimhF-)y^(_bTj~>N5ZTH z?~e$~KJcyNRz_*Kynh&t6{@VNx`lf*C}@@b%9Q}>fe6;n(7=o}92FvORegd@@MLm^ zN2OOD{#FIH*LnWP)ph<^f3JPX>_NdA;#(fD&3`Ul7nD?zc4IJF3PY?Y@!sZ|h16g;!$B0nXf;RN5b!tJgK zL%zRlpiti61WX1X215mfogsChAR0hL`YGp)ulUL;GavSt1GqyOX0c!-rhAXg1h_Lk zx23>SO!kjI5^`4oA_NEkK@7A_5;JXhPwL|>e-QE^p;115Y{Fo$p0)E+va+%iRljPO zFavTNi$Z?>JaixY+`N{(FcFW1GTD0zvzSl<~~27arec+;daoPKyB%3TeZ(>t^wWZQ+4XA&-r6M#{?rP zwZ4YsV$$LzlNSN4``a?xg&6eA2?pp!pb0DH_JO%hzG)hNTm$3eCaBSajOw_BXr5x?n>g#5Zqy0lOT> zlK?z#FG7BL1GrVdh9=PJ-EXG>Vt(-8!P-k38>Kf5oxE^8EsbD4337|UFapmn`RKrU zct;O*E+wo%6O8>(z(m25`#b3G1iAtVb&mizu?SRp3Wds_`9G$@nx7=;Hnz6TC~1uW z;*JoAf#h8WhN0oa&=wrwGBVIbn3D0rBkuRZ9PWBoxaOyVX%Q3@1emY&qHY*$LYwQU zPl;f=uJ-UyB5-w!Za75{wDNb%_ynRG*oJEd?sZ7Z*e2>(e_7A|3w-9rmX?!A-{)Z0 zbL2pF)o1$XXr22Yh|)^$AqC?j5k`N6khnXec?&(OhQRowIcG)MiSK@2G1#xSa) zWn&t7GxW8VE)9VZ^;9heY?55fS<^V9kqkDWRTHEofgKZ^TcAkQJUl39g$A6CP(sTb zdTvQzap18LFdUW?xdh0*0Kb6=2N^A~>x6a(R$M1L3O1}7)Uo|twGxC=`5WN@jF*<7 zVa#oh`o)81IaN`}<5xH{)#?bqxAeHNtppiMorx>aK2JqBTE@UTNpvbw`12qAG{XP) wy9U3`{NMd4hJRdd5)k%}Pyg}L58|F&hI&M0j^J#kObY+juiIpjv&K2(UzumGaR2}S diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail.png b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/images/thumbnail.png deleted file mode 100644 index dde48ae0246d0051a789d2cd1fa2ad2fd1ebeefc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11158 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Ȑ_)ZL#G61vJ}s;^4Qbp=3M;m=?|CFjTGa3 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 import('../ReactNVD3'), - loadBuildQuery: () => import('./buildQuery'), - metadata, - transformProps, - controlPanel, - }); - } -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts deleted file mode 100644 index 4d57ecddc2cb..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformData.ts +++ /dev/null @@ -1,179 +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 { DTTM_ALIAS } from '@superset-ui/core'; - -const DAY_MS = 24 * 60 * 60 * 1000; -const WEEKDAYS = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']; - -/** - * Rolls a timestamp back to the start of its period the way pandas - * offset.rollback(normalize=True) does for the offset aliases the - * frequency control offers (AS/A, QS/Q, MS/M, W and W-XXX, D, H, T/MIN). - * The multiplier prefix (e.g. 52W-MON) does not change the anchor. - */ -export const rollback = (timestamp: number, freq: string): number => { - const match = /^\d*(AS|YS|A|Y|QS|Q|MS|M|W(?:-([A-Z]{3}))?|D|H|T|MIN)$/i.exec( - (freq || 'W-MON').trim(), - ); - const date = new Date(timestamp); - const midnight = Date.UTC( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - ); - if (!match) { - return midnight; // freeform rules fall back to day boundaries - } - const unit = match[1].toUpperCase(); - if (unit === 'AS' || unit === 'YS') { - return Date.UTC(date.getUTCFullYear(), 0, 1); - } - if (unit === 'A' || unit === 'Y') { - // most recent Dec 31 at or before the timestamp - const yearEnd = Date.UTC(date.getUTCFullYear(), 11, 31); - return midnight >= yearEnd - ? yearEnd - : Date.UTC(date.getUTCFullYear() - 1, 11, 31); - } - if (unit === 'QS') { - return Date.UTC( - date.getUTCFullYear(), - Math.floor(date.getUTCMonth() / 3) * 3, - 1, - ); - } - if (unit === 'Q') { - // most recent quarter end at or before the timestamp - const quarterEndMonth = Math.floor(date.getUTCMonth() / 3) * 3 + 3; - const quarterEnd = Date.UTC(date.getUTCFullYear(), quarterEndMonth, 0); - return midnight >= quarterEnd - ? quarterEnd - : Date.UTC(date.getUTCFullYear(), quarterEndMonth - 3, 0); - } - if (unit === 'MS') { - return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1); - } - if (unit === 'M') { - // most recent month end at or before the timestamp - const monthEnd = Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0); - return midnight >= monthEnd - ? monthEnd - : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 0); - } - if (unit.startsWith('W')) { - const anchor = match[2] ? WEEKDAYS.indexOf(match[2].toUpperCase()) : 6; // W defaults to W-SUN - // JS getUTCDay: 0=SUN..6=SAT; WEEKDAYS index: 0=MON..6=SUN - const jsAnchor = anchor === 6 ? 0 : anchor + 1; - const diff = (date.getUTCDay() - jsAnchor + 7) % 7; - return midnight - diff * DAY_MS; - } - if (unit === 'D') { - return midnight; - } - if (unit === 'H') { - return timestamp - (timestamp % (60 * 60 * 1000)); - } - return timestamp - (timestamp % (60 * 1000)); // T / MIN -}; - -export interface TimePivotSeries { - key: string; - values: { x: number; y: number | null }[]; - rank: number; - perc: number; -} - -/** - * Ports the legacy NVD3TimePivotViz.get_data reshape: timestamps are - * bucketed into periods by the freq offset, ranked most-recent-first - * ("current", "-1", "-2", ...), shifted onto the latest period's time - * axis, and pivoted into one series per period with rank/perc metadata. - */ -export default function transformData( - records: Record[], - metricLabel: string, - freq: string, -): TimePivotSeries[] { - const rows = records - .filter(record => record[DTTM_ALIAS] != null) - .map(record => { - const timestamp = record[DTTM_ALIAS] as number; - return { - timestamp, - period: rollback(timestamp, freq), - value: record[metricLabel] as number | null, - }; - }); - if (rows.length === 0) { - return []; - } - - const periods = Array.from(new Set(rows.map(row => row.period))).sort( - (a, b) => b - a, - ); - const maxPeriod = periods[0]; - const rankOf = new Map(periods.map((period, index) => [period, index])); - const maxRank = periods.length - 1; - - const seriesOf = (rank: number) => (rank === 0 ? 'current' : `-${rank}`); - - // shift every point onto the latest period's time axis - const values = new Map>(); - const shiftedTimestamps = new Set(); - rows.forEach(({ timestamp, period, value }) => { - const series = seriesOf(rankOf.get(period)!); - const shifted = timestamp + (maxPeriod - period); - shiftedTimestamps.add(shifted); - if (!values.has(series)) { - values.set(series, new Map()); - } - values.get(series)!.set(shifted, value); - }); - - const timestamps = Array.from(shiftedTimestamps).sort((a, b) => a - b); - // pandas pivot sorts the series labels lexicographically - const seriesLabels = Array.from(values.keys()).sort(); - - const chartData: TimePivotSeries[] = []; - seriesLabels.forEach(series => { - const seriesValues = values.get(series)!; - let nonNullCount = 0; - const points = timestamps.map(timestamp => { - let y = seriesValues.has(timestamp) ? seriesValues.get(timestamp)! : null; - if (typeof y === 'number' && Number.isNaN(y)) { - y = null; - } - if (y != null) { - nonNullCount += 1; - } - return { x: timestamp, y }; - }); - if (nonNullCount === 0) { - return; - } - const rank = series === 'current' ? 0 : Number(series.slice(1)); - chartData.push({ - key: series, - values: points, - rank, - perc: 1 - rank / (maxRank + 1), - }); - }); - return chartData; -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts b/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts deleted file mode 100644 index 62739c13f5de..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/TimePivot/transformProps.ts +++ /dev/null @@ -1,39 +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 { ChartProps, getMetricLabel } from '@superset-ui/core'; -import nvd3TransformProps from '../transformProps'; -import transformData from './transformData'; - -export default function transformProps(chartProps: ChartProps) { - const { formData, queriesData } = chartProps; - const rawData = queriesData[0].data; - // v1 responses arrive as flat records; the legacy explore_json endpoint - // delivered the period-pivoted series directly. - const data = Array.isArray(rawData) - ? transformData( - rawData as Record[], - getMetricLabel(formData.metric), - formData.freq as string, - ) - : rawData; - return nvd3TransformProps({ - ...chartProps, - queriesData: [{ ...queriesData[0], data }], - } as ChartProps); -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/index.ts b/superset-frontend/plugins/preset-chart-nvd3/src/index.ts deleted file mode 100644 index e49f353cb951..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/index.ts +++ /dev/null @@ -1,22 +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. - */ - -export { default as NVD3ChartPreset } from './preset'; -export { default as BulletChartPlugin } from './Bullet'; -export { default as TimePivotChartPlugin } from './TimePivot'; diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/preset.ts b/superset-frontend/plugins/preset-chart-nvd3/src/preset.ts deleted file mode 100644 index 19f918092dfe..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/preset.ts +++ /dev/null @@ -1,33 +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 { Preset, VizType } from '@superset-ui/core'; -import BulletChartPlugin from './Bullet'; -import TimePivotChartPlugin from './TimePivot'; - -export default class NVD3ChartPreset extends Preset { - constructor() { - super({ - name: 'NVD3 charts', - plugins: [ - new BulletChartPlugin().configure({ key: VizType.Bullet }), - new TimePivotChartPlugin().configure({ key: VizType.TimePivot }), - ], - }); - } -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts b/superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts deleted file mode 100644 index 6bf7fc46a9a2..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/transformProps.ts +++ /dev/null @@ -1,215 +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. - */ -// @ts-nocheck -- legacy transformProps with loosely-typed formData from ChartProps -import { ChartProps, getMetricLabel, VizType } from '@superset-ui/core'; -import isTruthy from './utils/isTruthy'; -import { - tokenizeToNumericArray, - tokenizeToStringArray, -} from './utils/tokenize'; -import { formatLabel } from './utils'; - -const NOOP = () => {}; - -interface DatasourceMetric { - d3format?: string; - metric_name?: string; -} - -interface NVD3Datasource { - metrics?: DatasourceMetric[]; - verboseMap?: Record; -} - -const grabD3Format = ( - datasource: NVD3Datasource | undefined, - targetMetric: string, -): string | undefined => { - let foundFormatter: string | undefined; - const { metrics = [] } = datasource || {}; - metrics.forEach(metric => { - if (metric.d3format && metric.metric_name === targetMetric) { - foundFormatter = metric.d3format; - } - }); - - return foundFormatter; -}; - -export default function transformProps(chartProps: ChartProps) { - const { - width, - height, - annotationData, - datasource, - formData, - hooks, - queriesData, - } = chartProps; - - const { onAddFilter = NOOP, onError = NOOP } = hooks; - - const { - annotationLayers, - barStacked, - bottomMargin, - colorPicker, - colorScheme, - comparisonType, - contribution, - donut, - entity, - labelsOutside, - leftMargin, - lineInterpolation, - maxBubbleSize, - metric, - orderBars, - pieLabelType, - reduceXTicks, - richTooltip, - sendTimeRange, - showBarValue, - showBrush, - showControls, - showLabels, - showLegend, - showMarkers, - size, - stackedStyle, - vizType, - x, - xAxisFormat, - xAxisLabel, - xAxisShowminmax, - xLogScale, - xTicksLayout, - y, - yAxisBounds, - yAxis2Bounds, - yAxisLabel, - yAxisFormat, - yAxisShowminmax, - yAxis2Showminmax, - yLogScale, - sliceId, - } = formData; - - let { - markerLabels, - markerLines, - markerLineLabels, - markers, - numberFormat, - rangeLabels, - ranges, - } = formData; - - const rawData = queriesData[0].data || []; - let data; - if (vizType === VizType.Bullet && Array.isArray(rawData)) { - // The legacy explore_json endpoint reshaped the single-metric result - // into `{ measures: [...] }` server-side; the v1 chart data endpoint - // returns plain records, so the reshape happens here. - const metricLabel = getMetricLabel(metric); - data = { measures: rawData.map(row => row[metricLabel]) }; - } else { - data = Array.isArray(rawData) - ? rawData.map(row => ({ - ...row, - values: Array.isArray(row.values) - ? row.values.map(value => ({ ...value })) - : row.values, - key: formatLabel(row.key, datasource.verboseMap), - })) - : rawData; - } - - if (vizType === VizType.Pie) { - numberFormat = numberFormat || grabD3Format(datasource, metric); - } else if (vizType === VizType.Bullet) { - ranges = tokenizeToNumericArray(ranges) || [0, data.measures * 1.1]; - rangeLabels = tokenizeToStringArray(rangeLabels); - markerLabels = tokenizeToStringArray(markerLabels); - markerLines = tokenizeToNumericArray(markerLines); - markerLineLabels = tokenizeToStringArray(markerLineLabels); - markers = tokenizeToNumericArray(markers); - } - - return { - width, - height, - data, - annotationData, - annotationLayers, - areaStackedStyle: stackedStyle, - baseColor: colorPicker, - bottomMargin, - colorScheme, - comparisonType, - contribution, - entity, - isBarStacked: barStacked, - isDonut: donut, - isPieLabelOutside: labelsOutside, - leftMargin, - lineInterpolation, - markerLabels, - markerLines, - markerLineLabels, - markers, - maxBubbleSize: parseInt(maxBubbleSize, 10), - numberFormat, - onBrushEnd: isTruthy(sendTimeRange) - ? timeRange => { - onAddFilter('__time_range', timeRange, false, true); - } - : undefined, - onError, - orderBars, - pieLabelType, - rangeLabels, - ranges, - reduceXTicks, - showBarValue, - showBrush, - showControls, - showLabels, - showLegend, - showMarkers, - sizeField: size, - useRichTooltip: richTooltip, - vizType, - xAxisFormat, - xAxisLabel, - xAxisShowMinMax: xAxisShowminmax, - xField: x, - xIsLogScale: xLogScale, - xTicksLayout, - yAxisFormat, - yAxisBounds, - yAxis2Bounds, - yAxisLabel, - yAxisShowMinMax: yAxisShowminmax, - yAxis2ShowMinMax: yAxis2Showminmax, - yField: y, - yIsLogScale: yLogScale, - sliceId, - }; -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/utils.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils.ts deleted file mode 100644 index 04e235e01dae..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/utils.ts +++ /dev/null @@ -1,354 +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. - */ -// @ts-nocheck -- legacy file heavily dependent on untyped d3 v3 and nvd3 APIs -import d3 from 'd3'; -import d3tip from 'd3-tip'; -import dompurify from 'dompurify'; -import { - SMART_DATE_ID, - getTimeFormatter, - getNumberFormatter, -} from '@superset-ui/core'; -// Regexp for the label added to time shifted series -// (1 hour offset, 2 days offset, etc.) -const TIME_SHIFT_PATTERN = /\d+ \w+ offset/; - -const ANIMATION_TIME = 1000; - -export function cleanColorInput(value) { - // for superset series that should have the same color - return String(value) - .trim() - .replace(' (right axis)', '') - .split(', ') - .filter(k => !TIME_SHIFT_PATTERN.test(k)) - .join(', '); -} - -/** - * If format is smart_date, format date - * Otherwise, format number with the given format name - * @param {*} format - */ -export function getTimeOrNumberFormatter(format) { - return format === SMART_DATE_ID - ? getTimeFormatter(SMART_DATE_ID) - : getNumberFormatter(format); -} - -export function drawBarValues(svg, data, stacked, axisFormat) { - const format = getNumberFormatter(axisFormat); - const countSeriesDisplayed = data.filter(d => !d.disabled).length; - const totalStackedValues = - stacked && data.length !== 0 - ? data[0].values.map((bar, iBar) => { - const bars = data - .filter(series => !series.disabled) - .map(series => series.values[iBar]); - - return d3.sum(bars, d => d.y); - }) - : []; - svg.selectAll('.bar-chart-label-group').remove(); - setTimeout(() => { - svg.selectAll('.bar-chart-label-group').remove(); - const groupLabels = svg - .select('g.nv-barsWrap') - .append('g') - .attr('class', 'bar-chart-label-group'); - svg - .selectAll('g.nv-group') - .filter((d, i) => !stacked || i === countSeriesDisplayed - 1) - .selectAll('rect') - .each(function each(d, index) { - const rectObj = d3.select(this); - const transformAttr = rectObj.attr('transform'); - const xPos = parseFloat(rectObj.attr('x')); - const yPos = parseFloat(rectObj.attr('y')); - const rectWidth = parseFloat(rectObj.attr('width')); - const rectHeight = parseFloat(rectObj.attr('height')); - const textEls = groupLabels - .append('text') - .text(format(stacked ? totalStackedValues[index] : d.y)) - .attr('transform', transformAttr) - .attr('class', 'bar-chart-label'); - - // fine tune text position - const bbox = textEls.node().getBBox(); - const labelWidth = bbox.width; - const labelHeight = bbox.height; - textEls.attr('x', xPos + rectWidth / 2 - labelWidth / 2); - if (rectObj.attr('class').includes('positive')) { - textEls.attr('y', yPos - 5); - } else { - textEls.attr('y', yPos + rectHeight + labelHeight); - } - }); - }, ANIMATION_TIME); -} - -// Formats the series key to account for a possible NULL value -function getFormattedKey(seriesKey, shouldDompurify) { - if (seriesKey === '') { - return `<${seriesKey.slice(1, -1)}>`; - } - - return shouldDompurify ? dompurify.sanitize(seriesKey) : seriesKey; -} - -export function generateCompareTooltipContent(d, valueFormatter) { - let tooltip = ''; - tooltip += - "'; - d.series.sort((a, b) => (a.value >= b.value ? -1 : 1)); - d.series.forEach(series => { - const key = getFormattedKey(series.key, true); - tooltip += - `` + - `' + - `` + - `` + - ''; - }); - tooltip += '
" + - `${d.value}` + - '
` + - '
' + - '
${key}${valueFormatter(series.value)}
'; - - return dompurify.sanitize(tooltip); -} - -export function generateMultiLineTooltipContent(d, xFormatter, yFormatters) { - const tooltipTitle = xFormatter(d.value); - let tooltip = ''; - - tooltip += - "'; - - d.series.forEach((series, i) => { - const yFormatter = yFormatters[i]; - const key = getFormattedKey(series.key, true); - tooltip += - "` + - `` + - ``; - }); - - tooltip += '
" + - `${tooltipTitle}` + - '
" + - `
${key}${yFormatter(series.value)}
'; - - return dompurify.sanitize(tooltip); -} - -export function generateTimePivotTooltip(d, xFormatter, yFormatter) { - const tooltipTitle = xFormatter(d.value); - let tooltip = ''; - - tooltip += - "'; - - d.series.forEach(series => { - if (series.highlight) { - let label = ''; - if (series.key === 'current') { - label = series.key; - } else { - label = `${series.key} of the selected frequency:`; - } - tooltip += - "` + - `` + - ``; - } - }); - - tooltip += '
" + - `${tooltipTitle}` + - '
" + - `
${label}${yFormatter(series.value)}
'; - - return dompurify.sanitize(tooltip); -} - -function getLabel(stringOrObjectWithLabel) { - return stringOrObjectWithLabel.label || stringOrObjectWithLabel; -} - -function createHTMLRow(col1, col2) { - return `${col1}${col2}`; -} - -export function generateBubbleTooltipContent({ - point, - entity, - xField, - yField, - sizeField, - xFormatter, - yFormatter, - sizeFormatter, -}) { - let s = ''; - s += - `'; - s += createHTMLRow(getLabel(xField), xFormatter(point.x)); - s += createHTMLRow(getLabel(yField), yFormatter(point.y)); - s += createHTMLRow(getLabel(sizeField), sizeFormatter(point.size)); - s += '
` + - `${point[entity]} (${point.group})` + - '
'; - - return dompurify.sanitize(s); -} - -// shouldRemove indicates whether the nvtooltips should be removed from the DOM -export function hideTooltips(shouldRemove) { - const targets = document.querySelectorAll('.nvtooltip'); - if (targets.length > 0) { - // Only set opacity to 0 when hiding tooltips so they would reappear - // on hover, which sets the opacity to 1 - targets.forEach(t => { - if (shouldRemove) { - t.remove(); - } else { - // eslint-disable-next-line no-param-reassign - t.style.opacity = 0; - } - }); - } -} - -export function generateTooltipClassName(uuid) { - return `tooltip-${uuid}`; -} - -export function removeTooltip(uuid) { - const classSelector = `.${generateTooltipClassName(uuid)}`; - const target = document.querySelector(classSelector); - if (target) { - target.remove(); - } -} - -export function wrapTooltip(chart) { - const tooltipLayer = - chart.useInteractiveGuideline && chart.useInteractiveGuideline() - ? chart.interactiveLayer - : chart; - const tooltipGeneratorFunc = tooltipLayer.tooltip.contentGenerator(); - tooltipLayer.tooltip.contentGenerator(d => { - // The nvd3-fork default contentGenerator builds tooltip HTML with - // unescaped series keys (and feeds them into the tooltip's `.html()` - // sink at render time). Run the final string through DOMPurify so - // charts that do NOT install a custom contentGenerator (Line, Bar, - // Area, Pie, BoxPlot, etc.) cannot execute stored payloads in - // column or series names. Custom contentGenerators set elsewhere in - // this module already return sanitized output, making this a - // belt-and-braces wrap. - const tooltip = `

`; - return dompurify.sanitize(tooltip); - }); -} - -// Builds the sanitized HTML for an annotation layer's tooltip. Title and -// description values come from the annotation data source, so the output is -// run through dompurify before being inserted into the DOM by d3-tip. -export function generateAnnotationTooltipContent(layer, d) { - const title = - d[layer.titleColumn] && d[layer.titleColumn].length > 0 - ? `${d[layer.titleColumn]} - ${layer.name}` - : layer.name; - const body = Array.isArray(layer.descriptionColumns) - ? layer.descriptionColumns.map(c => d[c]) - : Object.values(d); - - return dompurify.sanitize( - `
${title}

${body.join(', ')}
`, - ); -} - -export function tipFactory(layer) { - return d3tip() - .attr('class', `d3-tip ${layer.annotationTipClass || ''}`) - .direction('n') - .offset([-5, 0]) - .html(d => (d ? generateAnnotationTooltipContent(layer, d) : '')); -} - -export function getMaxLabelSize(svg, axisClass) { - // axis class = .nv-y2 // second y axis on dual line chart - // axis class = .nv-x // x axis on time series line chart - const tickTexts = svg.selectAll(`.${axisClass} g.tick text`); - if (tickTexts.length > 0) { - const lengths = tickTexts[0].map(text => text.getComputedTextLength()); - - return Math.ceil(Math.max(0, ...lengths)); - } - - return 0; -} - -export function formatLabel(input, verboseMap = {}) { - // The input for label may be a string or an array of string - // When using the time shift feature, the label contains a '---' in the array - const verboseLookup = s => verboseMap[s] || s; - - return Array.isArray(input) && input.length > 0 - ? input - .map(l => (TIME_SHIFT_PATTERN.test(l) ? l : verboseLookup(l))) - .join(', ') - : verboseLookup(input); -} - -export function stringifyTimeRange(extent) { - if (extent.some(d => d.toISOString === undefined)) { - return null; - } - - return extent.map(d => d.toISOString().slice(0, -1)).join(' : '); -} - -export function setAxisShowMaxMin(axis, showminmax) { - if (axis && axis.showMaxMin && showminmax !== undefined) { - axis.showMaxMin(showminmax); - } -} - -export function computeYDomain(data) { - if (Array.isArray(data) && data.length > 0 && Array.isArray(data[0].values)) { - const extents = data - .filter(d => !d.disabled) - .map(row => d3.extent(row.values, v => v.y)); - const minOfMin = d3.min(extents, ([min]) => min); - const maxOfMax = d3.max(extents, ([, max]) => max); - - return [minOfMin, maxOfMax]; - } - - return [0, 1]; -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts deleted file mode 100644 index d06eafe1b234..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/utils/isTruthy.ts +++ /dev/null @@ -1,28 +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. - */ -export default function isTruthy(obj: unknown): boolean { - if (typeof obj === 'boolean') { - return obj; - } - if (typeof obj === 'string') { - return ['yes', 'y', 'true', 't', '1'].includes(obj.toLowerCase()); - } - - return !!obj; -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts b/superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts deleted file mode 100644 index 996d3cc824dc..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/utils/tokenize.ts +++ /dev/null @@ -1,33 +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 { validateNumber } from '@superset-ui/core'; - -export function tokenizeToNumericArray(value?: string): number[] | null { - if (!value?.trim()) return null; - const tokens = value.split(','); - if (tokens.some(token => validateNumber(token))) - throw new Error('All values should be numeric'); - return tokens.map(token => parseFloat(token)); -} - -export function tokenizeToStringArray(value?: string): string[] | null { - if (!value?.trim()) return null; - const tokens = value.split(','); - return tokens.map(token => token.trim()); -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts b/superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts deleted file mode 100644 index bafea70f29b2..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts +++ /dev/null @@ -1,86 +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. - */ -// @ts-nocheck -- vendor file; not fully typed -import { t } from '@apache-superset/core/translation'; - -function extractTypes(metadata) { - return Object.keys(metadata).reduce((prev, key) => { - const result = prev; - result[key] = key; - - return result; - }, {}); -} - -export const ANNOTATION_TYPES_METADATA = { - FORMULA: { - value: 'FORMULA', - label: t('Formula'), - }, - EVENT: { - value: 'EVENT', - label: t('Event'), - supportNativeSource: true, - }, - INTERVAL: { - value: 'INTERVAL', - label: t('Interval'), - supportNativeSource: true, - }, - TIME_SERIES: { - value: 'TIME_SERIES', - label: t('Time Series'), - }, -}; - -export const ANNOTATION_TYPES = extractTypes(ANNOTATION_TYPES_METADATA); - -export const DEFAULT_ANNOTATION_TYPE = ANNOTATION_TYPES.FORMULA; - -export const ANNOTATION_SOURCE_TYPES_METADATA = { - NATIVE: { - value: 'NATIVE', - label: 'Superset annotation', - }, -}; - -export const ANNOTATION_SOURCE_TYPES = extractTypes( - ANNOTATION_SOURCE_TYPES_METADATA, -); - -export function requiresQuery(annotationSourceType) { - return !!annotationSourceType; -} - -const NATIVE_COLUMN_NAMES = { - descriptionColumns: ['long_descr'], - intervalEndColumn: 'end_dttm', - timeColumn: 'start_dttm', - titleColumn: 'short_descr', -}; - -export function applyNativeColumns(annotation) { - if (annotation.sourceType === ANNOTATION_SOURCE_TYPES.NATIVE) { - return { ...annotation, ...NATIVE_COLUMN_NAMES }; - } - - return annotation; -} - -export default ANNOTATION_TYPES; diff --git a/superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts deleted file mode 100644 index 18a682f078ee..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/test/Bullet.test.ts +++ /dev/null @@ -1,98 +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 { supersetTheme } from '@apache-superset/core/theme'; -import { ChartProps, QueryFormData, VizType } from '@superset-ui/core'; -import buildQuery from '../src/Bullet/buildQuery'; -import transformProps from '../src/transformProps'; - -const formData: QueryFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - time_range: 'No filter', - viz_type: VizType.Bullet, - metric: 'sum__num', -}; - -describe('Bullet buildQuery', () => { - test('selects the single ungrouped metric', () => { - const [query] = buildQuery(formData).queries; - expect(query.metrics).toEqual(['sum__num']); - expect(query.columns).toEqual([]); - }); - - test('maps legacy granularity_sqla saved form data', () => { - const [query] = buildQuery(formData).queries; - expect(query.granularity).toEqual('ds'); - }); -}); - -describe('Bullet transformProps', () => { - const baseChartProps = { - width: 800, - height: 600, - datasource: { verboseMap: {} }, - theme: supersetTheme, - hooks: {}, - initialValues: {}, - }; - - test('reshapes v1 records into the measures shape', () => { - const chartProps = new ChartProps({ - ...baseChartProps, - formData: { vizType: VizType.Bullet, metric: 'sum__num' }, - queriesData: [{ data: [{ sum__num: 42 }] }], - }); - const { data } = transformProps(chartProps) as { - data: { measures: number[] }; - }; - expect(data).toEqual({ measures: [42] }); - }); - - test('passes through legacy-shaped measures payloads', () => { - const chartProps = new ChartProps({ - ...baseChartProps, - formData: { vizType: VizType.Bullet, metric: 'sum__num' }, - queriesData: [{ data: { measures: [42] } }], - }); - const { data } = transformProps(chartProps) as { - data: { measures: number[] }; - }; - expect(data).toEqual({ measures: [42] }); - }); - - test('reshapes using adhoc metric labels', () => { - const chartProps = new ChartProps({ - ...baseChartProps, - formData: { - vizType: VizType.Bullet, - metric: { - expressionType: 'SIMPLE', - aggregate: 'SUM', - column: { column_name: 'num' }, - label: 'SUM(num)', - }, - }, - queriesData: [{ data: [{ 'SUM(num)': 7 }] }], - }); - const { data } = transformProps(chartProps) as { - data: { measures: number[] }; - }; - expect(data).toEqual({ measures: [7] }); - }); -}); diff --git a/superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts deleted file mode 100644 index 7e2f6ec87e20..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/test/TimePivot.test.ts +++ /dev/null @@ -1,95 +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 { QueryFormData } from '@superset-ui/core'; -import buildQuery from '../src/TimePivot/buildQuery'; -import transformData, { rollback } from '../src/TimePivot/transformData'; - -test('buildQuery builds a timeseries query with the single metric', () => { - const formData: QueryFormData = { - datasource: '5__table', - granularity_sqla: 'ds', - time_range: 'Last quarter', - viz_type: 'time_pivot', - metric: 'sum__num', - freq: 'W-MON', - }; - const [query] = buildQuery(formData).queries; - expect(query.metrics).toEqual(['sum__num']); - expect(query.is_timeseries).toBe(true); -}); - -describe('TimePivot rollback', () => { - const wed = Date.UTC(2024, 0, 10, 15, 30); // Wednesday 2024-01-10 - test.each([ - ['W-MON', Date.UTC(2024, 0, 8)], - ['52W-MON', Date.UTC(2024, 0, 8)], - ['W-SUN', Date.UTC(2024, 0, 7)], - ['D', Date.UTC(2024, 0, 10)], - ['AS', Date.UTC(2024, 0, 1)], - ['QS', Date.UTC(2024, 0, 1)], - ['MS', Date.UTC(2024, 0, 1)], - ])('rolls back to the %s period start', (freq, expected) => { - expect(rollback(wed, freq as string)).toEqual(expected); - }); - - test('rolls back to the most recent month end for M', () => { - expect(rollback(wed, 'M')).toEqual(Date.UTC(2023, 11, 31)); - expect(rollback(Date.UTC(2024, 0, 31), 'M')).toEqual(Date.UTC(2024, 0, 31)); - }); -}); - -describe('TimePivot transformData', () => { - const mon1 = Date.UTC(2024, 0, 1); // Monday - const tue1 = Date.UTC(2024, 0, 2); - const mon2 = Date.UTC(2024, 0, 8); // next Monday - const tue2 = Date.UTC(2024, 0, 9); - - test('pivots periods onto the latest period axis with ranks', () => { - const data = transformData( - [ - { __timestamp: mon1, sum__num: 1 }, - { __timestamp: tue1, sum__num: 2 }, - { __timestamp: mon2, sum__num: 3 }, - { __timestamp: tue2, sum__num: 4 }, - ], - 'sum__num', - 'W-MON', - ); - expect(data.map(series => series.key)).toEqual(['-1', 'current']); - const previous = data[0]; - const current = data[1]; - expect(previous.rank).toEqual(1); - expect(previous.perc).toEqual(0.5); - expect(current.rank).toEqual(0); - expect(current.perc).toEqual(1); - // the older week is shifted onto the current week's timestamps - expect(previous.values).toEqual([ - { x: mon2, y: 1 }, - { x: tue2, y: 2 }, - ]); - expect(current.values).toEqual([ - { x: mon2, y: 3 }, - { x: tue2, y: 4 }, - ]); - }); - - test('returns an empty list for empty input', () => { - expect(transformData([], 'sum__num', 'W-MON')).toEqual([]); - }); -}); diff --git a/superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json b/superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json deleted file mode 100644 index 42fa49ba5149..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/test/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": false, - "emitDeclarationOnly": false, - "rootDir": "." - }, - "extends": "../../../tsconfig.json", - "include": ["**/*", "../types/**/*", "../../../types/**/*"] -} diff --git a/superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts b/superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts deleted file mode 100644 index 04c3f799787d..000000000000 --- a/superset-frontend/plugins/preset-chart-nvd3/test/utils.test.ts +++ /dev/null @@ -1,358 +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 { - getTimeFormatterRegistry, - SMART_DATE_ID, - createSmartDateFormatter, -} from '@superset-ui/core'; - -import { - computeYDomain, - generateAnnotationTooltipContent, - generateBubbleTooltipContent, - generateMultiLineTooltipContent, - getTimeOrNumberFormatter, - formatLabel, - tipFactory, -} from '../src/utils'; - -const DATA = [ - { - key: ['East Asia & Pacific'], - values: [ - { - x: -315619200000.0, - y: 1031863394.0, - }, - { - x: -283996800000.0, - y: 1034767718.0, - }, - ], - }, - { - key: ['South Asia'], - values: [ - { - x: -315619200000.0, - y: 572036107.0, - }, - { - x: -283996800000.0, - y: 584143236.0, - }, - ], - }, - { - key: ['Europe & Central Asia'], - values: [ - { - x: -315619200000.0, - y: 660881033.0, - }, - { - x: -283996800000.0, - y: 668526708.0, - }, - ], - }, -]; - -const DATA_WITH_DISABLED_SERIES = [ - { - disabled: true, - key: ['East Asia & Pacific'], - values: [ - { - x: -315619200000.0, - y: 1031863394.0, - }, - { - x: -283996800000.0, - y: 1034767718.0, - }, - ], - }, - { - disabled: true, - key: ['South Asia'], - values: [ - { - x: -315619200000.0, - y: 572036107.0, - }, - { - x: -283996800000.0, - y: 584143236.0, - }, - ], - }, - { - key: ['Europe & Central Asia'], - values: [ - { - x: -315619200000.0, - y: 660881033.0, - }, - { - x: -283996800000.0, - y: 668526708.0, - }, - ], - }, -]; - -describe('nvd3/utils', () => { - beforeEach(() => { - getTimeFormatterRegistry().registerValue( - SMART_DATE_ID, - createSmartDateFormatter(), - ); - }); - - describe('generateMultiLineTooltipContent()', () => { - const identity = (value: any) => value; - - test('renders the series key in the tooltip markup', () => { - const tooltip = generateMultiLineTooltipContent( - { - value: 'x-value', - series: [{ key: 'Region A', color: '#fff', value: 1 }], - }, - identity, - [identity], - ); - expect(tooltip).toContain('Region A'); - }); - - test('strips a script payload from a malicious series key', () => { - const tooltip = generateMultiLineTooltipContent( - { - value: 'x-value', - series: [ - { - key: '', - color: '#fff', - value: 1, - }, - ], - }, - identity, - [identity], - ); - // DOMPurify removes the event handler that would execute on render. - expect(tooltip).not.toContain('onerror'); - expect(tooltip).not.toContain('alert(1)'); - }); - }); - - describe('getTimeOrNumberFormatter(format)', () => { - test('is a function', () => { - expect(typeof getTimeOrNumberFormatter).toBe('function'); - }); - test('returns a date formatter if format is smart_date', () => { - const time = new Date(Date.UTC(2018, 10, 21, 22, 11)); - // @ts-expect-error -- getTimeOrNumberFormatter doesn't distinguish return types; accepts Date at runtime - expect(getTimeOrNumberFormatter('smart_date')(time)).toBe('10:11'); - }); - test('returns a number formatter otherwise', () => { - expect(getTimeOrNumberFormatter('.3s')(3000000)).toBe('3.00M'); - expect(getTimeOrNumberFormatter(undefined)(3000100)).toBe('3M'); - }); - }); - - describe('formatLabel()', () => { - const verboseMap = { - foo: 'Foo', - bar: 'Bar', - }; - - test('formats simple labels', () => { - expect(formatLabel('foo')).toBe('foo'); - expect(formatLabel(['foo'])).toBe('foo'); - expect(formatLabel(['foo', 'bar'])).toBe('foo, bar'); - }); - test('formats simple labels with lookups', () => { - expect(formatLabel('foo', verboseMap)).toBe('Foo'); - expect(formatLabel('baz', verboseMap)).toBe('baz'); - expect(formatLabel(['foo'], verboseMap)).toBe('Foo'); - expect(formatLabel(['foo', 'bar', 'baz'], verboseMap)).toBe( - 'Foo, Bar, baz', - ); - }); - test('deals with time shift properly', () => { - expect(formatLabel(['foo', '1 hour offset'], verboseMap)).toBe( - 'Foo, 1 hour offset', - ); - expect( - formatLabel(['foo', 'bar', 'baz', '2 hours offset'], verboseMap), - ).toBe('Foo, Bar, baz, 2 hours offset'); - }); - }); - - describe('computeYDomain()', () => { - test('works with invalid data', () => { - expect(computeYDomain('foo')).toEqual([0, 1]); - }); - - test('works with all series enabled', () => { - expect(computeYDomain(DATA)).toEqual([572036107.0, 1034767718.0]); - }); - - test('works with some series disabled', () => { - expect(computeYDomain(DATA_WITH_DISABLED_SERIES)).toEqual([ - 660881033.0, 668526708.0, - ]); - }); - }); - - // ------------------------------------------------------------------ - // Tooltip HTML sanitisation (XSS regression). - // Each helper below feeds user-controlled column values into a - // d3 / nvd3 .html() sink; the sanitised return must strip dangerous - // markup so a stored payload cannot execute on hover. - // ------------------------------------------------------------------ - - describe('generateBubbleTooltipContent() sanitises user input', () => { - test('strips ', - x: 1, - y: 2, - size: 3, - }, - entity: 'entity', - xField: 'x', - yField: 'y', - sizeField: 'size', - xFormatter: (v: number) => String(v), - yFormatter: (v: number) => String(v), - sizeFormatter: (v: number) => String(v), - }); - expect(html).not.toMatch(/', - color: 'red', - value: 1, - }, - ], - }, - (v: number) => String(v), - [(v: number) => String(v)], - ); - expect(html).not.toMatch(/payload', - }; - const html = tip.html()(datum); - expect(html).not.toMatch(/', - }); - expect(html).not.toContain('

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