diff --git a/.loki/reference/chrome_laptop_static_viz_ComboChart_Safari_Non_Iana_Timezone_Repro_44128.png b/.loki/reference/chrome_laptop_static_viz_ComboChart_Safari_Non_Iana_Timezone_Repro_44128.png new file mode 100644 index 0000000000000000000000000000000000000000..77e851e0ab03ac8b1b69f7fdfb74cb7fb055cb33 Binary files /dev/null and b/.loki/reference/chrome_laptop_static_viz_ComboChart_Safari_Non_Iana_Timezone_Repro_44128.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Auto_Y_Axis_Exclude_Zero_With_Goal.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Auto_Y_Axis_Exclude_Zero_With_Goal.png index 817205b9109ce76ba9c9de0065001f7ec59d5f57..d7cf26ae4c2f5eb7b9e67667902258981fed1d7e 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Auto_Y_Axis_Exclude_Zero_With_Goal.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Auto_Y_Axis_Exclude_Zero_With_Goal.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Bubble_Size.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Bubble_Size.png index 72c12fd24e819567a9c069f5ffcb7e0476b5f2be..3b6c9a55aef71a974e8ecc5b48cd518103e08225 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Bubble_Size.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Bubble_Size.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range.png index fec9a41274f0c141e3453b6f67e74b932ab7baf9..b6171cbca9421f10ceb70de20b33418cedc2a6f0 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range_With_Column_Scaling.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range_With_Column_Scaling.png index 9e9050ba17f143b4876eb7d30cd75fafd88558e3..aa99581b6449fdd59442fc51b5ac90dbf63ab568 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range_With_Column_Scaling.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Custom_Y_Axis_Range_With_Column_Scaling.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Default.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Default.png index 47a3e1cf90211839a726eb2d0bbeef921036255a..a1dd32200b21564b1e4c368d04ddc7195fdd2f52 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Default.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Default.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Goal_Line.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Goal_Line.png index 74ff856b52fca4e7c2a05760060aecedef5de120..0004430a8f9e10743b70f339948383a0bf268139 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Goal_Line.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Goal_Line.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Histogram_X_Scale.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Histogram_X_Scale.png index 28c0c2eea028fd7612592f3dccf52da3b18d542c..c0143dc5d8c8909b90dc464ab9c61b482dac9883 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Histogram_X_Scale.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Histogram_X_Scale.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale.png index d89d7561e24698ad70ab5c2c3299c7cd04593383..276e32158b1af1c062f2bc8273182b9a0161298d 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale_At_One.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale_At_One.png index 925ff52be6efe85c224b6efc8ad44b1babea1498..bcd2eea965b1039fbe39a051381eea4c23d31b08 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale_At_One.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Log_X_Scale_At_One.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout.png index 1dfc079918bec92322cd667b62a5abfec3095779..f13497bb6a0be2dc9055f3f9edd37f6f05981aba 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout_Bubble_Size.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout_Bubble_Size.png index 1f302631555245a282420a79946d3672b6f2ca75..fc6c2da5149dda6eaa94855dea2b90a1aa073e33 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout_Bubble_Size.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Dimension_Breakout_Bubble_Size.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Metric_Series.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Metric_Series.png index e3497dec04108437ecbb21a54dee584479786283..b0df2c704a8d223355d4265dce793363245458e4 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Metric_Series.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Multi_Metric_Series.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Ordinal_X_Scale.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Ordinal_X_Scale.png index ebbadd64d2243e959d75b8cf3c1ef7ad6367319d..619c43d0ff7db38afc0529a12bd3060f8f2f0dd0 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Ordinal_X_Scale.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Ordinal_X_Scale.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale.png index 7a5db44ff5e9686b7a5d284830a13a14cf10e3b3..11a9cd2c90e9f55bd354888e519294c9e1fb656a 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale_Multi_Series.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale_Multi_Series.png index 378c1b8f1f691a01c95ec746a1d7f2e0a8cfca9b..bb632f931ca851f2cbc52e8c831aaa6e4840fe6e 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale_Multi_Series.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Power_X_Scale_Multi_Series.png differ diff --git a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Timeseries_X_Scale.png b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Timeseries_X_Scale.png index ed4f2f0d115168f6b0a0b67a26ed030d428f0a84..92a43880ef02625c0933bc40eec5f0dc0f617f1f 100644 Binary files a/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Timeseries_X_Scale.png and b/.loki/reference/chrome_laptop_static_viz_ScatterPlot_Timeseries_X_Scale.png differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Default.png b/.loki/reference/chrome_laptop_viz_LineChart_Default.png index 4d3030b04f80cd3f1fdd96d57c7287896f1cea1b..fec1060892e9b1d92dda5a2d346c2a1ec7d2e4ef 100644 Binary files a/.loki/reference/chrome_laptop_viz_LineChart_Default.png and b/.loki/reference/chrome_laptop_viz_LineChart_Default.png differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png index 14b197ab2b87b64bd1a0aaa417f41c5bd90814f0..a313f12bc813a6a5e4df116c6985639e52de4cf2 100644 Binary files a/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png and b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png differ diff --git a/.loki/reference/chrome_laptop_viz_TableSimple_Embedding_Theme.png b/.loki/reference/chrome_laptop_viz_TableSimple_Embedding_Theme.png index d45d01ba7d5ff26b604c122c2440f29d18cc7d1a..d95b073c090c18ff07c43f711bb75e6976a25a65 100644 Binary files a/.loki/reference/chrome_laptop_viz_TableSimple_Embedding_Theme.png and b/.loki/reference/chrome_laptop_viz_TableSimple_Embedding_Theme.png differ diff --git a/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx b/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx index da78ec20edea1371825ecad497e4ec10821a5c98..ff3ca53902f0ed2775ab4ddb9036c2b1b5e3532b 100644 --- a/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx @@ -801,6 +801,13 @@ AreaChartSteppedNullsSkipped.args = { renderingContext, }; +export const SafariNonIanaTimezoneRepro44128 = Template.bind({}); +SafariNonIanaTimezoneRepro44128.args = { + rawSeries: data.safariNonIanaTimezoneRepro44128 as any, + dashcardSettings: {}, + renderingContext, +}; + export const Default = Template.bind({}); Default.args = { rawSeries: data.messedUpAxis as any, diff --git a/frontend/src/metabase/static-viz/components/ComboChart/stories-data/index.ts b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/index.ts index bb74f0de7c0b9190fe28b60f91e3cad5eec8a241..a62c81eaa3b4a3fa91aa4664c4df04ded5312db4 100644 --- a/frontend/src/metabase/static-viz/components/ComboChart/stories-data/index.ts +++ b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/index.ts @@ -88,6 +88,7 @@ import nullCategoryValueFormatting from "./null-category-value-formatting.json"; import numberOfInsightsError39608 from "./number-of-insights-error-39608.json"; import numericXAxisIncludesZero37082 from "./numeric-x-axis-includes-zero-37082.json"; import powYScaleCustomYAxisRange from "./pow-y-scale-custom-y-axis-range.json"; +import safariNonIanaTimezoneRepro44128 from "./safari-non-iana-timezone-repro-44128.json"; import ticksNativeWeekWithGapLongRange from "./ticks-native-week-with-gap-long-range.json"; import ticksNativeWeekWithGapShortRange from "./ticks-native-week-with-gap-short-range.json"; import timeSeriesTicksCompactFormattingMixedTimezones from "./time-series-ticks-compact-formatting-mixed-timezones.json"; @@ -220,4 +221,5 @@ export const data = { comboDataLabelsAutoCompactnessPropagatesFromTotals, areaChartSteppedNullsInterpolated, areaChartSteppedNullsSkipped, + safariNonIanaTimezoneRepro44128, }; diff --git a/frontend/src/metabase/static-viz/components/ComboChart/stories-data/safari-non-iana-timezone-repro-44128.json b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/safari-non-iana-timezone-repro-44128.json new file mode 100644 index 0000000000000000000000000000000000000000..fe25213369cd339a6b53b710631c5b0a28c31820 --- /dev/null +++ b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/safari-non-iana-timezone-repro-44128.json @@ -0,0 +1,266 @@ +[ + { + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 0, + "collection_position": null, + "table_id": null, + "can_run_adhoc_query": true, + "result_metadata": [ + { + "display_name": "CURRENT_DATE", + "field_ref": [ + "field", + "CURRENT_DATE", + { + "base-type": "type/Date" + } + ], + "name": "CURRENT_DATE", + "base_type": "type/Date", + "effective_type": "type/Date", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 3, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2024-06-14T01:18:55Z", + "latest": "2024-06-16T01:18:55Z" + } + } + } + }, + { + "display_name": "VALUE", + "field_ref": [ + "field", + "VALUE", + { + "base-type": "type/Decimal" + } + ], + "name": "VALUE", + "base_type": "type/Decimal", + "effective_type": "type/Decimal", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 3, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 5, + "q1": 6.25, + "q3": 17.5, + "max": 20, + "sd": 7.637626158259733, + "avg": 11.666666666666666 + } + } + } + } + ], + "creator": { + "email": "aleksandr@metabase.com", + "first_name": "Aleksandr", + "last_login": "2024-06-14T01:08:07.309972Z", + "is_qbnewb": true, + "is_superuser": true, + "id": 1, + "last_name": "Lesnenko", + "date_joined": "2024-06-14T00:54:52.277777Z", + "common_name": "Aleksandr Lesnenko" + }, + "initially_published_at": null, + "can_write": true, + "database_id": 2, + "enable_embedding": false, + "collection_id": null, + "query_type": "native", + "name": "oracle-repro", + "last_query_start": null, + "dashboard_count": 0, + "last_used_at": null, + "type": "question", + "average_query_time": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2024-06-14T01:21:31.634213Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "database": 2, + "type": "native", + "native": { + "template-tags": {}, + "query": "SELECT CURRENT_DATE, 10 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+1, 20 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+2, 5 AS VALUE FROM DUAL" + } + }, + "id": 27, + "parameter_mappings": [], + "display": "line", + "entity_id": "uNceD8IzYwC7Brg7LxIbc", + "collection_preview": true, + "last-edit-info": { + "timestamp": "2024-06-14T01:21:32.940Z", + "id": 1, + "first_name": "Aleksandr", + "last_name": "Lesnenko", + "email": "aleksandr@metabase.com" + }, + "visualization_settings": { + "graph.dimensions": ["CURRENT_DATE"], + "graph.metrics": ["VALUE"] + }, + "collection": null, + "metabase_version": "v0.50.3 (855535b)", + "parameters": [], + "created_at": "2024-06-14T01:21:31.634213Z", + "parameter_usage_count": 0, + "public_uuid": null + }, + "data": { + "rows": [ + ["2024-06-14T01:18:55Z", 10], + ["2024-06-15T01:18:55Z", 20], + ["2024-06-16T01:18:55Z", 5] + ], + "cols": [ + { + "display_name": "CURRENT_DATE", + "source": "native", + "field_ref": [ + "field", + "CURRENT_DATE", + { + "base-type": "type/Date" + } + ], + "name": "CURRENT_DATE", + "base_type": "type/Date", + "effective_type": "type/Date" + }, + { + "display_name": "VALUE", + "source": "native", + "field_ref": [ + "field", + "VALUE", + { + "base-type": "type/Decimal" + } + ], + "name": "VALUE", + "base_type": "type/Decimal", + "effective_type": "type/Decimal" + } + ], + "native_form": { + "params": null, + "query": "SELECT CURRENT_DATE, 10 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+1, 20 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+2, 5 AS VALUE FROM DUAL" + }, + "format-rows?": true, + "results_timezone": "+01:15", + "results_metadata": { + "columns": [ + { + "display_name": "CURRENT_DATE", + "field_ref": [ + "field", + "CURRENT_DATE", + { + "base-type": "type/Date" + } + ], + "name": "CURRENT_DATE", + "base_type": "type/Date", + "effective_type": "type/Date", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 3, + "nil%": 0 + }, + "type": { + "type/DateTime": { + "earliest": "2024-06-14T01:18:55Z", + "latest": "2024-06-16T01:18:55Z" + } + } + } + }, + { + "display_name": "VALUE", + "field_ref": [ + "field", + "VALUE", + { + "base-type": "type/Decimal" + } + ], + "name": "VALUE", + "base_type": "type/Decimal", + "effective_type": "type/Decimal", + "semantic_type": null, + "fingerprint": { + "global": { + "distinct-count": 3, + "nil%": 0 + }, + "type": { + "type/Number": { + "min": 5, + "q1": 6.25, + "q3": 17.5, + "max": 20, + "sd": 7.637626158259733, + "avg": 11.666666666666666 + } + } + } + } + ] + }, + "insights": [ + { + "previous-value": 20, + "unit": "day", + "offset": 49734.30367476851, + "last-change": -0.75, + "col": "VALUE", + "slope": -2.5, + "last-value": 5, + "best-fit": ["+", 49734.30367476851, ["*", -2.5, "x"]] + } + ] + }, + "cached": false, + "database_id": 2, + "started_at": "2024-06-14T01:18:55.312056Z", + "json_query": { + "database": 2, + "type": "native", + "native": { + "query": "SELECT CURRENT_DATE, 10 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+1, 20 AS VALUE FROM DUAL\nUNION ALL\nSELECT CURRENT_DATE+2, 5 AS VALUE FROM DUAL", + "template-tags": {} + }, + "middleware": { + "js-int-to-string?": true, + "userland-query?": true, + "add-default-userland-constraints?": true + } + }, + "average_execution_time": null, + "status": "completed", + "context": "ad-hoc", + "row_count": 3, + "running_time": 345 + } +] diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts b/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts index cd2ae08857978b43ca48de330f049451843753fc..f19dddda07603e1bdebc88b04504542986349be4 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts @@ -38,7 +38,7 @@ import type { } from "metabase/visualizations/echarts/cartesian/model/types"; import { computeTimeseriesDataInverval, - getTimezone, + getTimezoneOrOffset, minTimeseriesUnit, tryGetDate, } from "metabase/visualizations/echarts/cartesian/utils/timeseries"; @@ -644,7 +644,11 @@ export function getTimeSeriesXAxisModel( dimensionModel, showWarning, ); - const { interval: dataTimeSeriesInterval, timezone } = timeSeriesInfo; + const { + interval: dataTimeSeriesInterval, + timezone, + offsetMinutes, + } = timeSeriesInfo; const formatter = (value: RowValue, unit?: DateTimeAbsoluteUnit) => { const formatUnit = unit ?? @@ -674,7 +678,13 @@ export function getTimeSeriesXAxisModel( if (!date) { return null; } - return date.tz(timezone).format("YYYY-MM-DDTHH:mm:ss[Z]"); + + const dateInTimezone = + offsetMinutes != null + ? date.add(offsetMinutes, "minute") + : date.tz(timezone); + + return dateInTimezone.format("YYYY-MM-DDTHH:mm:ss[Z]"); }; const fromEChartsAxisValue = (rawValue: number) => { return dayjs.utc(rawValue); @@ -863,7 +873,10 @@ function getTimeSeriesXAxisInfo( .map(column => (isAbsoluteDateTimeUnit(column.unit) ? column.unit : null)) .filter(isNotNull), ); - const timezone = getTimezone(rawSeries, showWarning); + const { timezone, offsetMinutes } = getTimezoneOrOffset( + rawSeries, + showWarning, + ); const interval = (computeTimeseriesDataInverval(xValues, unit) ?? { count: 1, unit: "day", @@ -883,7 +896,7 @@ function getTimeSeriesXAxisInfo( intervalsCount = Math.ceil(max.diff(min, interval.unit) / interval.count); } - return { interval, timezone, intervalsCount, range, unit }; + return { interval, timezone, offsetMinutes, intervalsCount, range, unit }; } export function getScaledMinAndMax( diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/model/types.ts b/frontend/src/metabase/visualizations/echarts/cartesian/model/types.ts index 2116cdb43126896899e58c96b59abd4ce9ee5994..0b5f8a73839f60e2e8710624375b6ab38bee35d6 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/model/types.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/model/types.ts @@ -155,7 +155,8 @@ export type TimeSeriesXAxisModel = BaseXAxisModel & TimeSeriesAxisScaleTransforms & { axisType: "time"; columnUnit?: DateTimeAbsoluteUnit; - timezone: string; + timezone?: string; + offsetMinutes?: number; interval: TimeSeriesInterval; intervalsCount: number; range: DateRange; diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts index 1bc1efee040d71cae0f5c4ab1ce67f28cc73dbd7..0edab3325fa585f09b65f5e19df640ae5530202c 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts @@ -249,10 +249,32 @@ export function getLargestInterval(intervals: TimeSeriesInterval[]) { }); } +// Tests for offsets like +01:15, -09:45 +const OFFSET_PATTERN = /^([+-])(\d{2}):(\d{2})$/; + +const tryParseOffsetMinutes = (maybeOffset: string): number | undefined => { + const match = maybeOffset.match(OFFSET_PATTERN); + + if (!match) { + return undefined; + } + + const [, sign, hours, minutes] = match; + const offsetSign = sign === "+" ? 1 : -1; + const offsetHours = parseInt(hours, 10); + const offsetMinutes = parseInt(minutes, 10); + const totalOffsetMinutes = (offsetHours * 60 + offsetMinutes) * offsetSign; + + return totalOffsetMinutes; +}; + // We should always have results_timezone, but just in case we fallback to UTC export const DEFAULT_TIMEZONE = "Etc/UTC"; -export function getTimezone(series: RawSeries, showWarning?: ShowWarning) { +export function getTimezoneOrOffset( + series: RawSeries, + showWarning?: ShowWarning, +): { timezone?: string; offsetMinutes?: number } { // Dashboard multiseries cards might have series with different timezones. const timezones = Array.from( new Set(series.map(s => s.data.results_timezone)), @@ -268,5 +290,16 @@ export function getTimezone(series: RawSeries, showWarning?: ShowWarning) { ); } - return results_timezone || DEFAULT_TIMEZONE; + const offsetMinutes = + results_timezone != null + ? tryParseOffsetMinutes(results_timezone) + : undefined; + + const timezone = + offsetMinutes == null ? results_timezone || DEFAULT_TIMEZONE : undefined; + + return { + timezone, + offsetMinutes, + }; } diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.unit.spec.js b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.unit.spec.js index b1e54d21ba5ae659bdf9e46b2d3486a62ee44eb5..abf61bbc94fdac861a1770a85b868475cc0ae532 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.unit.spec.js +++ b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.unit.spec.js @@ -2,8 +2,8 @@ import { StringColumn, NumberColumn } from "__support__/visualizations"; import { getVisualizationTransformed } from "metabase/visualizations"; import { computeTimeseriesDataInverval, - getTimezone, computeTimeseriesTicksInterval, + getTimezoneOrOffset, } from "metabase/visualizations/echarts/cartesian/utils/timeseries"; import registerVisualizations from "metabase/visualizations/register"; @@ -139,29 +139,7 @@ describe("visualization.lib.timeseries", () => { }); }); - describe("getTimezone", () => { - const series = [ - { - card: { visualization_settings: {}, display: "bar" }, - data: { - results_timezone: "US/Eastern", - cols: [StringColumn({ name: "a" }), NumberColumn({ name: "b" })], - rows: [], - }, - }, - ]; - - it("should extract results_timezone", () => { - const timezone = getTimezone(series); - expect(timezone).toBe("US/Eastern"); - }); - - it("should extract results_timezone after series is transformed", () => { - const { series: transformed } = getVisualizationTransformed(series); - const timezone = getTimezone(transformed); - expect(timezone).toBe("US/Eastern"); - }); - }); + describe("getTimezone", () => {}); describe("computeTimeseriesTicksInterval", () => { // computeTimeseriesTicksInterval just uses tickFormat to measure the character length of the current formatting style @@ -240,4 +218,99 @@ describe("visualization.lib.timeseries", () => { }, ); }); + + describe("getTimezoneOrOffset", () => { + const showWarningMock = jest.fn(); + + const series = [ + { + card: { visualization_settings: {}, display: "bar" }, + data: { + results_timezone: "US/Eastern", + cols: [StringColumn({ name: "a" }), NumberColumn({ name: "b" })], + rows: [], + }, + }, + ]; + + beforeEach(() => { + showWarningMock.mockClear(); + }); + + it("should extract results_timezone", () => { + const { timezone } = getTimezoneOrOffset(series); + expect(timezone).toBe("US/Eastern"); + }); + + it("should extract results_timezone after series is transformed", () => { + const { series: transformed } = getVisualizationTransformed(series); + const { timezone } = getTimezoneOrOffset(transformed); + expect(timezone).toBe("US/Eastern"); + }); + + it("should return the correct timezone when there is only one timezone", () => { + const series = [ + { + data: { + results_timezone: "America/New_York", + requested_timezone: "America/New_York", + }, + }, + ]; + const result = getTimezoneOrOffset(series, showWarningMock); + expect(result).toEqual({ + timezone: "America/New_York", + offsetMinutes: undefined, + }); + expect(showWarningMock).not.toHaveBeenCalled(); + }); + + it("should return the default timezone when results_timezone is undefined", () => { + const series = [ + { + data: { + results_timezone: undefined, + requested_timezone: undefined, + }, + }, + ]; + const result = getTimezoneOrOffset(series, showWarningMock); + expect(result).toEqual({ + timezone: "Etc/UTC", + offsetMinutes: undefined, + }); + expect(showWarningMock).not.toHaveBeenCalled(); + }); + + it("should return offsetMinutes when results_timezone is in offset format", () => { + const series = [ + { data: { results_timezone: "+05:30", requested_timezone: "+05:30" } }, + ]; + const result = getTimezoneOrOffset(series, showWarningMock); + expect(result).toEqual({ timezone: undefined, offsetMinutes: 330 }); + expect(showWarningMock).not.toHaveBeenCalled(); + }); + + it("should show warning when there are multiple timezones in the series", () => { + const series = [ + { data: { results_timezone: "America/New_York" } }, + { data: { results_timezone: "Europe/London" } }, + ]; + getTimezoneOrOffset(series, showWarningMock); + expect(showWarningMock).toHaveBeenCalledTimes(1); + }); + + it("should show warning when requested_timezone is different from results_timezone", () => { + const series = [ + { + data: { + results_timezone: "America/New_York", + requested_timezone: "Europe/London", + }, + }, + ]; + getTimezoneOrOffset(series, showWarningMock); + expect(showWarningMock).toHaveBeenCalledTimes(1); + }); + }); });