Repository: metabase/metabase
Pull request #56466: fix bars width on dst timezone data
Author: login: alxnddr
Labels: name: .Team/DashViz
description: DEPRECATED  Please use .Team/UXWest instead
name: double-backport
Created: 2025-04-08T21:15:58Z | Merged: 2025-04-10T15:06:17Z

Closes https://github.com/metabase/metabase/issues/56424

### Description

On timeseries bar charts bar widths are computed based on the number of intervals within data e.g. number of full months in the dataset. The number of intervals was not calculated correctly when data is in a DST timezone caused by one missing hour.

### How to verify

Follow steps from the original issue https://github.com/metabase/metabase/issues/56424 but if you can't reproduce then go to Admin -> Localization -> Report Timezone and set Europe/Berlin.

### Demo

<img width="2557" alt="Screenshot 2025-04-08 at 5 14 07 PM" src="https://github.com/user-attachments/assets/488736bf-17e8-489b-8ed0-556da026d6c8" />

### Checklist

- [x] Tests have been added/updated to cover changes in this PR



Diff:
diff --git a/.loki/reference/chrome_laptop_Viz_Static_Viz_ComboChart_Bar_Width_Dst_Timezones_56424.png b/.loki/reference/chrome_laptop_Viz_Static_Viz_ComboChart_Bar_Width_Dst_Timezones_56424.png
new file mode 100644
index 000000000000..2ff479a5a42e
Binary files /dev/null and b/.loki/reference/chrome_laptop_Viz_Static_Viz_ComboChart_Bar_Width_Dst_Timezones_56424.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 88e14ae52cbb..08d5bd65eaae 100644
--- a/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx
+++ b/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx
@@ -1040,6 +1040,14 @@ export const BarNonLinearNumericXAxis = {
   },
 };
 
+export const BarWidthDstTimezones56424 = {
+  render: Template,
+  args: {
+    rawSeries: data.barWidthDstTimezones56424 as any,
+    renderingContext,
+  },
+};
+
 export const Default = {
   render: Template,
   args: {
diff --git a/frontend/src/metabase/static-viz/components/ComboChart/stories-data/bar-width-dst-timezones-56424.json b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/bar-width-dst-timezones-56424.json
new file mode 100644
index 000000000000..46cfb78bca48
--- /dev/null
+++ b/frontend/src/metabase/static-viz/components/ComboChart/stories-data/bar-width-dst-timezones-56424.json
@@ -0,0 +1,495 @@
+[
+  {
+    "data": {
+      "rows": [
+        ["2025-01-01T00:00:00+01:00", "a", 124],
+        ["2025-01-01T00:00:00+01:00", "b", 280],
+        ["2025-01-01T00:00:00+01:00", "c", 691],
+        ["2025-01-01T00:00:00+01:00", "d", 0],
+        ["2025-01-01T00:00:00+01:00", "e", 10],
+        ["2025-01-01T00:00:00+01:00", "f", 931],
+        ["2025-01-01T00:00:00+01:00", "g", 17],
+        ["2025-01-01T00:00:00+01:00", "h", 115],
+        ["2025-01-01T00:00:00+01:00", "i", 25],
+        ["2025-01-01T00:00:00+01:00", "j", 0],
+        ["2025-01-01T00:00:00+01:00", "k", 37],
+        ["2025-02-01T00:00:00+01:00", "a", 166],
+        ["2025-02-01T00:00:00+01:00", "b", 281],
+        ["2025-02-01T00:00:00+01:00", "c", 1002],
+        ["2025-02-01T00:00:00+01:00", "d", 0],
+        ["2025-02-01T00:00:00+01:00", "e", 58],
+        ["2025-02-01T00:00:00+01:00", "f", 983],
+        ["2025-02-01T00:00:00+01:00", "g", 102],
+        ["2025-02-01T00:00:00+01:00", "h", 104],
+        ["2025-02-01T00:00:00+01:00", "i", 10],
+        ["2025-02-01T00:00:00+01:00", "j", 0],
+        ["2025-02-01T00:00:00+01:00", "k", 35],
+        ["2025-03-01T00:00:00+01:00", "a", 557],
+        ["2025-03-01T00:00:00+01:00", "b", 282],
+        ["2025-03-01T00:00:00+01:00", "c", 926],
+        ["2025-03-01T00:00:00+01:00", "d", 0],
+        ["2025-03-01T00:00:00+01:00", "e", 59],
+        ["2025-03-01T00:00:00+01:00", "f", 1112],
+        ["2025-03-01T00:00:00+01:00", "g", 258],
+        ["2025-03-01T00:00:00+01:00", "h", 79],
+        ["2025-03-01T00:00:00+01:00", "i", 14],
+        ["2025-03-01T00:00:00+01:00", "k", 67],
+        ["2025-04-01T00:00:00+02:00", "a", 60],
+        ["2025-04-01T00:00:00+02:00", "b", 57],
+        ["2025-04-01T00:00:00+02:00", "c", 207],
+        ["2025-04-01T00:00:00+02:00", "d", 0],
+        ["2025-04-01T00:00:00+02:00", "e", 22],
+        ["2025-04-01T00:00:00+02:00", "f", 221],
+        ["2025-04-01T00:00:00+02:00", "g", 87],
+        ["2025-04-01T00:00:00+02:00", "h", 27],
+        ["2025-04-01T00:00:00+02:00", "i", 4],
+        ["2025-04-01T00:00:00+02:00", "k", 19]
+      ],
+      "results_timezone": "Europe/Berlin",
+      "download_perms": "full",
+      "results_metadata": {
+        "columns": [
+          {
+            "database_type": "timestamp",
+            "semantic_type": null,
+            "unit": "month",
+            "name": "date",
+            "field_ref": [
+              "field",
+              "date",
+              {
+                "base-type": "type/Date",
+                "temporal-unit": "month"
+              }
+            ],
+            "effective_type": "type/DateTime",
+            "id": 40281,
+            "visibility_type": "normal",
+            "display_name": "Date: Month",
+            "fingerprint": {
+              "global": {
+                "distinct-count": 4,
+                "nil%": 0
+              },
+              "type": {
+                "type/DateTime": {
+                  "earliest": "2025-01-01T00:00:00Z",
+                  "latest": "2025-04-01T00:00:00Z"
+                }
+              }
+            },
+            "base_type": "type/DateTime"
+          },
+          {
+            "database_type": "varchar",
+            "semantic_type": "type/Source",
+            "name": "subchannel",
+            "field_ref": [
+              "field",
+              "subchannel",
+              {
+                "base-type": "type/Text"
+              }
+            ],
+            "effective_type": "type/Text",
+            "id": 40282,
+            "visibility_type": "normal",
+            "display_name": "Subchannel",
+            "fingerprint": {
+              "global": {
+                "distinct-count": 11,
+                "nil%": 0
+              },
+              "type": {
+                "type/Text": {
+                  "percent-json": 0,
+                  "percent-url": 0,
+                  "percent-email": 0,
+                  "percent-state": 0,
+                  "average-length": 1
+                }
+              }
+            },
+            "base_type": "type/Text"
+          },
+          {
+            "display_name": "Sum of Leads",
+            "field_ref": ["aggregation", 0],
+            "base_type": "type/Decimal",
+            "effective_type": "type/Decimal",
+            "database_type": "numeric",
+            "name": "sum",
+            "semantic_type": null,
+            "fingerprint": {
+              "global": {
+                "distinct-count": 36,
+                "nil%": 0
+              },
+              "type": {
+                "type/Number": {
+                  "min": 0,
+                  "q1": 17,
+                  "q3": 258,
+                  "max": 1112,
+                  "sd": 323.3885745523289,
+                  "avg": 214.97619047619048
+                }
+              }
+            }
+          }
+        ]
+      },
+      "requested_timezone": "Europe/Berlin",
+      "format-rows?": true,
+      "cols": [
+        {
+          "database_type": "timestamp",
+          "table_id": 1721,
+          "unit": "month",
+          "name": "date",
+          "source": "breakout",
+          "field_ref": [
+            "field",
+            "date",
+            {
+              "base-type": "type/Date",
+              "temporal-unit": "month"
+            }
+          ],
+          "effective_type": "type/DateTime",
+          "active": true,
+          "id": 40281,
+          "ident": "PHR2eRmXhAUfNH30ogH5v",
+          "position": 1,
+          "visibility_type": "normal",
+          "display_name": "Date: Month",
+          "base_type": "type/DateTime"
+        },
+        {
+          "database_type": "varchar",
+          "semantic_type": "type/Source",
+          "table_id": 1721,
+          "name": "subchannel",
+          "source": "breakout",
+          "field_ref": [
+            "field",
+            "subchannel",
+            {
+              "base-type": "type/Text"
+            }
+          ],
+          "effective_type": "type/Text",
+          "active": true,
+          "id": 40282,
+          "ident": "l3kAtU2ttMaLZ-xFsopey",
+          "position": 3,
+          "visibility_type": "normal",
+          "display_name": "Subchannel",
+          "base_type": "type/Text"
+        },
+        {
+          "database_type": "numeric",
+          "name": "sum",
+          "source": "aggregation",
+          "field_ref": ["aggregation", 0],
+          "effective_type": "type/Decimal",
+          "aggregation_index": 0,
+          "ident": "dCNLoYho0mcnCevUJtTje",
+          "display_name": "Sum of Leads",
+          "base_type": "type/Decimal"
+        }
+      ],
+      "native_form": {
+        "query": "SELECT DATE_TRUNC('month', CAST(\"source\".\"date\" AS timestamp)) AS \"date\", \"source\".\"subchannel\" AS \"subchannel\", SUM(\"source\".\"leads\") AS \"sum\" FROM (SELECT \"public\".\"overlapping_bars_20250408122725\".\"_mb_row_id\" AS \"_mb_row_id\", \"public\".\"overlapping_bars_20250408122725\".\"date\" AS \"date\", \"public\".\"overlapping_bars_20250408122725\".\"leads\" AS \"leads\", \"public\".\"overlapping_bars_20250408122725\".\"subchannel\" AS \"subchannel\" FROM \"public\".\"overlapping_bars_20250408122725\") AS \"source\" GROUP BY DATE_TRUNC('month', CAST(\"source\".\"date\" AS timestamp)), \"source\".\"subchannel\" ORDER BY DATE_TRUNC('month', CAST(\"source\".\"date\" AS timestamp)) ASC, \"source\".\"subchannel\" ASC",
+        "params": null
+      },
+      "is_sandboxed": false,
+      "dataset": true,
+      "insights": [
+        {
+          "previous-value": null,
+          "unit": "month",
+          "offset": 20713.52108654698,
+          "last-change": null,
+          "col": "sum",
+          "slope": -1.0181769738897395,
+          "last-value": 19,
+          "best-fit": [
+            "+",
+            203057.5675457945,
+            ["*", -20468.28112699451, ["log", "x"]]
+          ]
+        }
+      ],
+      "model": true
+    },
+    "cached": null,
+    "database_id": 2,
+    "started_at": "2025-04-08T16:59:33.573645-04:00",
+    "json_query": {
+      "constraints": {
+        "max-results": 10000,
+        "max-results-bare-rows": 2000
+      },
+      "type": "query",
+      "middleware": {
+        "js-int-to-string?": true,
+        "ignore-cached-results?": false,
+        "process-viz-settings?": false,
+        "userland-query?": true
+      },
+      "cache-strategy": {
+        "multiplier": 10000,
+        "min_duration_ms": 1000,
+        "type": "ttl",
+        "avg-execution-ms": 97
+      },
+      "viz-settings": {
+        "stackable.stack_type": "stacked",
+        "graph.x_axis.scale": "timeseries",
+        "graph.dimensions": ["date", "subchannel"],
+        "graph.metrics": ["sum"]
+      },
+      "database": 2,
+      "query": {
+        "aggregation": [
+          [
+            "sum",
+            [
+              "field",
+              "leads",
+              {
+                "base-type": "type/BigInteger"
+              }
+            ]
+          ]
+        ],
+        "breakout": [
+          [
+            "field",
+            "date",
+            {
+              "base-type": "type/Date",
+              "temporal-unit": "month"
+            }
+          ],
+          [
+            "field",
+            "subchannel",
+            {
+              "base-type": "type/Text"
+            }
+          ]
+        ],
+        "source-table": "card__737",
+        "aggregation-idents": {
+          "0": "dCNLoYho0mcnCevUJtTje"
+        },
+        "breakout-idents": {
+          "0": "BnqR1aeDTNxVd9ks3Q5N5",
+          "1": "06dBBtJEBkpGFbKT_hhoY"
+        }
+      }
+    },
+    "average_execution_time": null,
+    "status": "completed",
+    "context": "question",
+    "row_count": 42,
+    "running_time": 79,
+    "card": {
+      "original_card_id": 739,
+      "can_delete": false,
+      "public_uuid": null,
+      "parameter_usage_count": 0,
+      "created_at": "2025-04-08T16:46:57.880798Z",
+      "dashboard_id": null,
+      "parameters": [],
+      "metabase_version": "v0.55.2-SNAPSHOT (f9b9c0a)",
+      "collection": {
+        "metabase.models.collection.root/is-root?": true,
+        "authority_level": null,
+        "name": "Our analytics",
+        "is_personal": false,
+        "id": "root",
+        "can_write": true
+      },
+      "visualization_settings": {
+        "stackable.stack_type": "stacked",
+        "graph.x_axis.scale": "timeseries",
+        "graph.dimensions": ["date", "subchannel"],
+        "graph.metrics": ["sum"]
+      },
+      "collection_preview": true,
+      "entity_id": "-x8PIhzHabtKQL-rvbbuL",
+      "archived_directly": false,
+      "display": "bar",
+      "can_manage_db": true,
+      "parameter_mappings": [],
+      "id": 739,
+      "dataset_query": {
+        "database": 2,
+        "type": "query",
+        "query": {
+          "aggregation": [
+            [
+              "sum",
+              [
+                "field",
+                "leads",
+                {
+                  "base-type": "type/BigInteger"
+                }
+              ]
+            ]
+          ],
+          "breakout": [
+            [
+              "field",
+              "date",
+              {
+                "base-type": "type/Date",
+                "temporal-unit": "month"
+              }
+            ],
+            [
+              "field",
+              "subchannel",
+              {
+                "base-type": "type/Text"
+              }
+            ]
+          ],
+          "source-table": "card__737",
+          "aggregation-idents": {
+            "0": "dCNLoYho0mcnCevUJtTje"
+          },
+          "breakout-idents": {
+            "0": "BnqR1aeDTNxVd9ks3Q5N5",
+            "1": "06dBBtJEBkpGFbKT_hhoY"
+          }
+        }
+      },
+      "cache_ttl": null,
+      "embedding_params": null,
+      "made_public_by_id": null,
+      "updated_at": "2025-04-08T16:46:57.880798Z",
+      "moderation_reviews": [],
+      "can_restore": false,
+      "creator_id": 1,
+      "average_query_time": 92.16666666666667,
+      "type": "question",
+      "dashboard": null,
+      "last_used_at": "2025-04-08T20:59:08.006893Z",
+      "dashboard_count": 0,
+      "last_query_start": "2025-04-08T20:59:07.972926Z",
+      "name": "56424",
+      "query_type": "query",
+      "collection_id": null,
+      "enable_embedding": false,
+      "database_id": 2,
+      "card_schema": 20,
+      "trashed_from_collection_id": null,
+      "can_write": true,
+      "initially_published_at": null,
+      "result_metadata": [
+        {
+          "database_type": "timestamp",
+          "semantic_type": null,
+          "unit": "month",
+          "name": "date",
+          "field_ref": [
+            "field",
+            "date",
+            {
+              "base-type": "type/Date",
+              "temporal-unit": "month"
+            }
+          ],
+          "effective_type": "type/DateTime",
+          "id": 40281,
+          "visibility_type": "normal",
+          "display_name": "Date: Month",
+          "fingerprint": {
+            "global": {
+              "distinct-count": 4,
+              "nil%": 0
+            },
+            "type": {
+              "type/DateTime": {
+                "earliest": "2025-01-01T00:00:00Z",
+                "latest": "2025-04-01T00:00:00Z"
+              }
+            }
+          },
+          "base_type": "type/DateTime"
+        },
+        {
+          "database_type": "varchar",
+          "semantic_type": "type/Source",
+          "name": "subchannel",
+          "field_ref": [
+            "field",
+            "subchannel",
+            {
+              "base-type": "type/Text"
+            }
+          ],
+          "effective_type": "type/Text",
+          "id": 40282,
+          "visibility_type": "normal",
+          "display_name": "Subchannel",
+          "fingerprint": {
+            "global": {
+              "distinct-count": 11,
+              "nil%": 0
+            },
+            "type": {
+              "type/Text": {
+                "percent-json": 0,
+                "percent-url": 0,
+                "percent-email": 0,
+                "percent-state": 0,
+                "average-length": 1
+              }
+            }
+          },
+          "base_type": "type/Text"
+        },
+        {
+          "display_name": "Sum of Leads",
+          "field_ref": ["aggregation", 0],
+          "base_type": "type/Decimal",
+          "effective_type": "type/Decimal",
+          "database_type": "numeric",
+          "name": "sum",
+          "semantic_type": null,
+          "fingerprint": {
+            "global": {
+              "distinct-count": 36,
+              "nil%": 0
+            },
+            "type": {
+              "type/Number": {
+                "min": 0,
+                "q1": 17,
+                "q3": 258,
+                "max": 1112,
+                "sd": 323.3885745523289,
+                "avg": 214.97619047619048
+              }
+            }
+          }
+        }
+      ],
+      "can_run_adhoc_query": true,
+      "table_id": 1721,
+      "source_card_id": 737,
+      "collection_position": null,
+      "view_count": 25,
+      "archived": false,
+      "description": null,
+      "cache_invalidated_at": null,
+      "displayIsLocked": true
+    }
+  }
+]
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 d1071f9103f3..6f8d97d696df 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
@@ -53,6 +53,7 @@ import barStackedSeriesLabelsRotated from "./bar-stacked-series-labels-rotated.j
 import barStackedTotalFormattedValues from "./bar-stacked-total-formatted-values.json";
 import barTwoAxesStackedWithNegativeValues from "./bar-two-axes-stacked-with-negative-values.json";
 import barTwoDaysOfWeek from "./bar-two-days-of-week.json";
+import barWidthDstTimezones56424 from "./bar-width-dst-timezones-56424.json";
 import barsBreakoutSortedWithNegativeValuesPowerYAxis from "./bars-breakout-sorted-with-negative-values-power-y-axis.json";
 import breakoutNullAndEmptyString from "./breakout-null-and-empty-string.json";
 import combinedBarTimeSeriesDifferentGranularityWithBreakout from "./combined-bar-timeseries-different-granularity-with-breakout.json";
@@ -252,4 +253,5 @@ export const data = {
   offsetBasedTimezone47835,
   noDimensionColumn54755,
   barNonLinearNumericXAxis,
+  barWidthDstTimezones56424,
 };
diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts b/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts
index 69590dd4e4e7..b18b8b782aff 100644
--- a/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts
+++ b/frontend/src/metabase/visualizations/echarts/cartesian/model/axis.ts
@@ -38,8 +38,10 @@ import type {
 } from "metabase/visualizations/echarts/cartesian/model/types";
 import {
   computeTimeseriesDataInverval,
+  getTimeSeriesIntervalDuration,
   getTimezoneOrOffset,
   minTimeseriesUnit,
+  normalizeDate,
   tryGetDate,
 } from "metabase/visualizations/echarts/cartesian/utils/timeseries";
 import { computeNumericDataInverval } from "metabase/visualizations/lib/numeric";
@@ -885,6 +887,11 @@ const getXAxisDateRangeFromSortedXAxisValues = (
   return [minDate, maxDate];
 };
 
+const DAY_INTERVAL: TimeSeriesInterval = {
+  count: 1,
+  unit: "day",
+};
+
 function getTimeSeriesXAxisInfo(
   xValues: RowValue[],
   rawSeries: RawSeries,
@@ -906,10 +913,7 @@ function getTimeSeriesXAxisInfo(
     rawSeries,
     showWarning,
   );
-  const interval = (computeTimeseriesDataInverval(xValues, unit) ?? {
-    count: 1,
-    unit: "day",
-  }) as TimeSeriesInterval;
+  const interval = computeTimeseriesDataInverval(xValues, unit) ?? DAY_INTERVAL;
 
   const range = getXAxisDateRangeFromSortedXAxisValues(xValues);
 
@@ -920,7 +924,19 @@ function getTimeSeriesXAxisInfo(
   let intervalsCount = 0;
 
   if (range) {
-    const [min, max] = range;
+    let [min, max] = range;
+
+    const intervalDurationMs = getTimeSeriesIntervalDuration(interval);
+    const isDayOrMore =
+      intervalDurationMs >= getTimeSeriesIntervalDuration(DAY_INTERVAL);
+
+    // If the interval is a day or more, normalize the dates to UTC to avoid
+    // the interference of timezone differences.
+    if (isDayOrMore) {
+      min = normalizeDate(min);
+      max = normalizeDate(max);
+    }
+
     // A single date counts as one interval
     intervalsCount = Math.abs(
       Math.ceil(max.diff(min, interval.unit) / interval.count),
diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts
index 1108e7035700..ad160e56647e 100644
--- a/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts
+++ b/frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts
@@ -302,3 +302,12 @@ export function getTimezoneOrOffset(
     offsetMinutes,
   };
 }
+
+export function normalizeDate(dayjsDate: Dayjs) {
+  return dayjs
+    .utc()
+    .year(dayjsDate.year())
+    .month(dayjsDate.month())
+    .date(dayjsDate.date())
+    .startOf("day");
+}
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 61caa5f627a2..aa166828d31c 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
@@ -1,9 +1,18 @@
+import dayjs from "dayjs";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
+
+// Enable timezone and UTC plugins
+dayjs.extend(utc);
+dayjs.extend(timezone);
+
 import { NumberColumn, StringColumn } from "__support__/visualizations";
 import { getVisualizationTransformed } from "metabase/visualizations";
 import {
   computeTimeseriesDataInverval,
   computeTimeseriesTicksInterval,
   getTimezoneOrOffset,
+  normalizeDate,
 } from "metabase/visualizations/echarts/cartesian/utils/timeseries";
 import registerVisualizations from "metabase/visualizations/register";
 
@@ -313,4 +322,24 @@ describe("visualization.lib.timeseries", () => {
       expect(showWarningMock).toHaveBeenCalledTimes(1);
     });
   });
+
+  describe("normalizeDate", () => {
+    it("should normalize date to UTC at start of day", () => {
+      const date = dayjs("2023-05-15T14:30:45");
+      const normalized = normalizeDate(date);
+      expect(normalized.format()).toBe("2023-05-15T00:00:00Z");
+    });
+
+    it("should handle date before and after DST transition", () => {
+      // Before DST: 1 AM EST
+      const beforeDST = dayjs.tz("2023-03-12 01:00:00", "America/New_York");
+      const normalizedBefore = normalizeDate(beforeDST);
+      expect(normalizedBefore.format()).toBe("2023-03-12T00:00:00Z");
+
+      // After DST: 3 AM EDT (after springing forward)
+      const afterDST = dayjs.tz("2023-03-12 03:00:00", "America/New_York");
+      const normalizedAfter = normalizeDate(afterDST);
+      expect(normalizedAfter.format()).toBe("2023-03-12T00:00:00Z");
+    });
+  });
 });


Reviews:
user: login: lorem--ipsum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T05:17:46Z
user: login: alxnddr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:21:07Z
user: login: alxnddr
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T17:22:06Z
user: login: lorem--ipsum
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T10:12:35Z

Review comments:
path: .loki/reference/chrome_laptop_Viz_Static_Viz_ComboChart_Bar_Width_Dst_Timezones_56424.png
user: login: lorem--ipsum
body: The bars aren't evenly spaced, is that expected?
created_at: 2025-04-09T05:15:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -302,3 +302,12 @@ export function getTimezoneOrOffset(
     offsetMinutes,
   };
 }
+
+export function normalizeDate(dayjsDate: Dayjs) {
path: frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts
user: login: lorem--ipsum
body: It'd be great to have at least a couple unit tests cases and some docs around this function. "Normalize" can mean different things based on the context, and if someone needs to modify this function in the future it'll be easier for them to know what it's supposed to do with tests (and incidentally it'll give quicker feedback if they break something).
created_at: 2025-04-09T05:17:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -302,3 +302,12 @@ export function getTimezoneOrOffset(
     offsetMinutes,
   };
 }
+
+export function normalizeDate(dayjsDate: Dayjs) {
path: frontend/src/metabase/visualizations/echarts/cartesian/utils/timeseries.ts
user: login: alxnddr
body: Good catch, I added specs, thanks!
created_at: 2025-04-09T17:21:07Z
author_association: MEMBER
path: .loki/reference/chrome_laptop_Viz_Static_Viz_ComboChart_Bar_Width_Dst_Timezones_56424.png
user: login: alxnddr
body: Yes, this is because it is a continuous timeseries scale and February is shorter than the rest
created_at: 2025-04-09T17:22:06Z
author_association: MEMBER

PR comments:
user: login: trunk-io[bot]
created_at: 2025-04-08T22:09:09Z
body: <!-- Trunk Test Analytics -->


[![Static Badge](https://img.shields.io/badge/18944_tests-1cc637ae-purple)](https://app.trunk.io/metabase/flaky-tests/pr/56466?repo=metabase/metabase&commitHash=1cc637ae034d2a86fcf7dd1521503235cd62d59c&utm_source=gh-comment) &ensp; [![Static Badge](https://img.shields.io/badge/0-failed-crimson)](https://app.trunk.io/metabase/flaky-tests/pr/56466?repo=metabase/metabase&commitHash=1cc637ae034d2a86fcf7dd1521503235cd62d59c&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-flaky-red)](https://app.trunk.io/metabase/flaky-tests/pr/56466?repo=metabase/metabase&commitHash=1cc637ae034d2a86fcf7dd1521503235cd62d59c&utm_source=gh-comment&runConclusion=FAILURE) &ensp; [![Static Badge](https://img.shields.io/badge/0-quarantined-yellow)](https://app.trunk.io/metabase/flaky-tests/pr/56466?repo=metabase/metabase&commitHash=1cc637ae034d2a86fcf7dd1521503235cd62d59c&utm_source=gh-comment&runConclusion=QUARANTINED)


<sub>

[View Full Report ↗︎](https://app.trunk.io/metabase/flaky-tests/pr/56466?repo=metabase/metabase&commitHash=1cc637ae034d2a86fcf7dd1521503235cd62d59c&utm_source=gh-comment) ⋅ [Docs](https://docs.trunk.io/flaky-tests?utm_source=gh-comment)

</sub>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-10T15:07:31Z
body: @alxnddr Manual conflict resolution is required on #56556
author_association: CONTRIBUTOR