Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Area chart migration and tweaks the Timeseries chart migration #25952

Merged
merged 6 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
[pytest]
testpaths =
tests
python_files = *_test.py test_*.py *_tests.py
python_files = *_test.py test_*.py *_tests.py *viz/utils.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes pytest display full assertion errors defined in that file.

1 change: 0 additions & 1 deletion superset/charts/commands/importers/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def migrate_chart(config: dict[str, Any]) -> dict[str, Any]:
if isclass(class_)
and issubclass(class_, MigrateViz)
and hasattr(class_, "source_viz_type")
and class_ != processors.MigrateAreaChart # incomplete
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the migration back to import function now that it's complete.

}

output = copy.deepcopy(config)
Expand Down
8 changes: 2 additions & 6 deletions superset/migrations/shared/migrate_viz/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ def upgrade(cls, session: Session) -> None:
slices = session.query(Slice).filter(Slice.viz_type == cls.source_viz_type)
for slc in paginated_update(
slices,
lambda current, total: print(
f" Updating {current}/{total} charts", end="\r"
),
lambda current, total: print(f"Upgraded {current}/{total} charts"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display a log entry for each block creating a log history.

):
new_viz = cls.upgrade_slice(slc)
session.merge(new_viz)
Expand All @@ -177,9 +175,7 @@ def downgrade(cls, session: Session) -> None:
)
for slc in paginated_update(
slices,
lambda current, total: print(
f" Downgrading {current}/{total} charts", end="\r"
),
lambda current, total: print(f"Downgraded {current}/{total} charts"),
):
new_viz = cls.downgrade_slice(slc)
session.merge(new_viz)
83 changes: 39 additions & 44 deletions superset/migrations/shared/migrate_viz/processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,40 +36,6 @@ def _pre_action(self) -> None:
self.data["metric"] = self.data["metrics"][0]


class MigrateAreaChart(MigrateViz):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the incomplete and unused migration.

"""
Migrate area charts.

This migration is incomplete, see https://github.com/apache/superset/pull/24703#discussion_r1265222611
for more details. If you fix this migration, please update the ``migrate_chart``
function in ``superset/charts/commands/importers/v1/utils.py`` so that it gets
applied in chart imports.
"""

source_viz_type = "area"
target_viz_type = "echarts_area"
remove_keys = {"contribution", "stacked_style", "x_axis_label"}

def _pre_action(self) -> None:
if self.data.get("contribution"):
self.data["contributionMode"] = "row"

if stacked := self.data.get("stacked_style"):
stacked_map = {
"expand": "Expand",
"stack": "Stack",
}
self.data["show_extra_controls"] = True
self.data["stack"] = stacked_map.get(stacked)

if x_axis := self.data.get("granularity_sqla"):
self.data["x_axis"] = x_axis

if x_axis_label := self.data.get("x_axis_label"):
self.data["x_axis_title"] = x_axis_label
self.data["x_axis_title_margin"] = 30


class MigratePivotTable(MigrateViz):
source_viz_type = "pivot_table"
target_viz_type = "pivot_table_v2"
Expand Down Expand Up @@ -137,10 +103,22 @@ class MigrateSunburst(MigrateViz):

class TimeseriesChart(MigrateViz):
has_x_axis_control = True
rename_keys = {
Copy link
Member Author

@michael-s-molina michael-s-molina Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not new. Just moved from MigrateLineChart.

"bottom_margin": "x_axis_title_margin",
"left_margin": "y_axis_title_margin",
"show_controls": "show_extra_controls",
"x_axis_label": "x_axis_title",
"x_axis_format": "x_axis_time_format",
"x_ticks_layout": "xAxisLabelRotation",
"y_axis_label": "y_axis_title",
"y_axis_showminmax": "truncateYAxis",
"y_log_scale": "logAxis",
}
remove_keys = {"contribution", "show_brush", "show_markers"}

def _pre_action(self) -> None:
self.data["contributionMode"] = "row" if self.data.get("contribution") else None
self.data["zoomable"] = self.data.get("show_brush") != "no"
self.data["zoomable"] = self.data.get("show_brush") == "yes"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michael-s-molina I see this is an update to the existing TimeseriesChart migration where the zoomable used to be True if show_brush was None whereas now it's False.

I'm not privy to knowing whether this logic (and that on lines 144–146) is correct, but maybe it make more sense to have said logic in a separate PR. Additionally is there anyway to proactively fix previously migrated charts which were wrong? I gather the x_ticks_layout field could be remedied (as it's persisted), but the show_brush was removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@john-bodley These PRs only add the migration logic but there's no migration invoking them directly. We'll have that in 4.0 when we forcefully remove the legacy versions. Right now, these migrations can only be invoked by the viz_migrations command which was not released yet. The command will be available in 3.1.

Copy link
Member

@john-bodley john-bodley Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@michael-s-molina could we change to PR description to be something of the form,

Add Area chart migration and tweaks the Timeseries chart migration

which will aid developers if they're trying to bisect when/where a possible change occurred.

self.data["markerEnabled"] = self.data.get("show_markers") or False
self.data["y_axis_showminmax"] = True

Expand All @@ -163,23 +141,19 @@ def _pre_action(self) -> None:
"difference" if comparison_type == "absolute" else comparison_type
)

if x_ticks_layout := self.data.get("x_ticks_layout"):
self.data["x_ticks_layout"] = 45 if x_ticks_layout == "45°" else 0


class MigrateLineChart(TimeseriesChart):
source_viz_type = "line"
target_viz_type = "echarts_timeseries_line"
rename_keys = {
Copy link
Member Author

@michael-s-molina michael-s-molina Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to TimeseriesChart.

"x_axis_label": "x_axis_title",
"bottom_margin": "x_axis_title_margin",
"x_axis_format": "x_axis_time_format",
"y_axis_label": "y_axis_title",
"left_margin": "y_axis_title_margin",
"y_axis_showminmax": "truncateYAxis",
"y_log_scale": "logAxis",
}

def _pre_action(self) -> None:
super()._pre_action()

self.remove_keys.add("line_interpolation")

line_interpolation = self.data.get("line_interpolation")
if line_interpolation == "cardinal":
self.target_viz_type = "echarts_timeseries_smooth"
Expand All @@ -189,3 +163,24 @@ def _pre_action(self) -> None:
elif line_interpolation == "step-after":
self.target_viz_type = "echarts_timeseries_step"
self.data["seriesType"] = "end"


class MigrateAreaChart(TimeseriesChart):
source_viz_type = "area"
target_viz_type = "echarts_area"
stacked_map = {
"expand": "Expand",
"stack": "Stack",
"stream": "Stream",
}

def _pre_action(self) -> None:
super()._pre_action()

self.remove_keys.add("stacked_style")

self.data["stack"] = self.stacked_map.get(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the stack key be None or undefined if the resulting key isn't in self.stacked_map?

Copy link
Member Author

@michael-s-molina michael-s-molina Nov 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It can be None or undefined as it's optional.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a preference or whether we explicitly define None? I guess there's likely so many inconsistencies on both the front- and back-end with regards to when/where to persist values that even if we make it undefined (and thus minimizing the payload) there's no guarantee that the field wont materialize with a value of None on save.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly 😆

self.data.get("stacked_style") or "stack"
)

self.data["opacity"] = 0.7

This file was deleted.

14 changes: 11 additions & 3 deletions tests/unit_tests/charts/commands/importers/v1/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@ def test_migrate_chart_area() -> None:
"description": None,
"certified_by": None,
"certification_details": None,
"viz_type": "area",
"viz_type": "echarts_area",
"query_context": None,
"params": json.dumps(
{
"adhoc_filters": [],
"adhoc_filters": [
{
"clause": "WHERE",
"subject": "ds",
"operator": "TEMPORAL_RANGE",
"comparator": "No filter",
"expressionType": "SIMPLE",
}
],
"annotation_layers": [],
"bottom_margin": "auto",
"x_axis_title_margin": "auto",
"color_scheme": "supersetColors",
"comparison_type": "values",
"dashboards": [],
Expand Down
39 changes: 5 additions & 34 deletions tests/unit_tests/migrations/viz/dual_line_to_mixed_chart_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import json
from typing import Any

from superset.migrations.shared.migrate_viz import MigrateDualLine
from tests.unit_tests.migrations.viz.utils import migrate_and_assert

ADHOC_FILTERS = [
{
Expand All @@ -28,7 +29,7 @@
}
]

SOURCE_FORM_DATA = {
SOURCE_FORM_DATA: dict[str, Any] = {
"metric": "num_boys",
"y_axis_format": ",d",
"y_axis_bounds": [50, 100],
Expand All @@ -42,7 +43,7 @@
"yAxisIndex": 0,
}

TARGET_FORM_DATA = {
TARGET_FORM_DATA: dict[str, Any] = {
"metrics": ["num_boys"],
"y_axis_format": ",d",
"y_axis_bounds": [50, 100],
Expand All @@ -64,34 +65,4 @@
def test_migration() -> None:
source = SOURCE_FORM_DATA.copy()
target = TARGET_FORM_DATA.copy()
upgrade_downgrade(source, target)


def upgrade_downgrade(source, target) -> None:
from superset.models.slice import Slice

dumped_form_data = json.dumps(source)

slc = Slice(
viz_type=MigrateDualLine.source_viz_type,
datasource_type="table",
params=dumped_form_data,
query_context=f'{{"form_data": {dumped_form_data}}}',
)

# upgrade
slc = MigrateDualLine.upgrade_slice(slc)

# verify form_data
new_form_data = json.loads(slc.params)
assert new_form_data == target
assert new_form_data["form_data_bak"] == source

# verify query_context
new_query_context = json.loads(slc.query_context)
assert new_query_context["form_data"]["viz_type"] == "mixed_timeseries"

# downgrade
slc = MigrateDualLine.downgrade_slice(slc)
assert slc.viz_type == MigrateDualLine.source_viz_type
assert json.loads(slc.params) == source
migrate_and_assert(MigrateDualLine, source, target)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the refactor.

42 changes: 42 additions & 0 deletions tests/unit_tests/migrations/viz/nvd3_area_chart_to_echarts_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# 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 MigrateAreaChart
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": "area",
"stacked_style": "stream",
}

TARGET_FORM_DATA: dict[str, Any] = {
"form_data_bak": SOURCE_FORM_DATA,
"viz_type": "echarts_area",
"opacity": 0.7,
"stack": "Stream",
}


def test_migration() -> None:
SOURCE_FORM_DATA.update(TIMESERIES_SOURCE_FORM_DATA)
TARGET_FORM_DATA.update(TIMESERIES_TARGET_FORM_DATA)
migrate_and_assert(MigrateAreaChart, SOURCE_FORM_DATA, TARGET_FORM_DATA)
Loading
Loading