diff --git a/e2e/test/scenarios/visualizations-charts/line-bar-tooltips.cy.spec.js b/e2e/test/scenarios/visualizations-charts/line-bar-tooltips.cy.spec.js index 99d93ead7e16c5d3ffd08e8798224ab591287489..78b580e66246537769e424f5f99adb5c32000241 100644 --- a/e2e/test/scenarios/visualizations-charts/line-bar-tooltips.cy.spec.js +++ b/e2e/test/scenarios/visualizations-charts/line-bar-tooltips.cy.spec.js @@ -593,6 +593,107 @@ describe("scenarios > visualizations > line/bar chart > tooltips", () => { testTooltipExcludesText("Compared to preivous month"); }); }); + + describe("> percent change across daylight savings time change", () => { + const SUM_OF_TOTAL_APRIL = { + name: "Q1", + query: { + "source-table": ORDERS_ID, + aggregation: [["sum", ["field", ORDERS.TOTAL, null]]], + breakout: [["field", ORDERS.CREATED_AT, { "temporal-unit": "month" }]], + filter: [ + "between", + ["field", 39, { "base-type": "type/DateTime" }], + "2024-01-01", + "2024-05-30", + ], + }, + display: "line", + }; + + const APRIL_CHANGES = [null, "-10.89%", "11.1%", "-2.89%"]; + + const SUM_OF_TOTAL_DST_WEEK = { + name: "Q1", + query: { + "source-table": ORDERS_ID, + aggregation: [["sum", ["field", ORDERS.TOTAL, null]]], + breakout: [["field", ORDERS.CREATED_AT, { "temporal-unit": "week" }]], + filter: [ + "between", + ["field", 39, { "base-type": "type/DateTime" }], + "2024-03-01", + "2024-03-31", + ], + }, + display: "line", + }; + + const DST_WEEK_CHANGES = [null, "191.48%", "4.76%", "-2.36%"]; + + const SUM_OF_TOTAL_DST_DAY = { + name: "Q1", + query: { + "source-table": ORDERS_ID, + aggregation: [["sum", ["field", ORDERS.TOTAL, null]]], + breakout: [["field", ORDERS.CREATED_AT, { "temporal-unit": "day" }]], + filter: [ + "between", + ["field", 39, { "base-type": "type/DateTime" }], + "2024-03-09", + "2024-03-12", + ], + }, + display: "line", + }; + + const DST_DAY_CHANGES = [null, "27.5%", "-26.16%"]; + + it("should not omit percent change on April", () => { + setup({ question: SUM_OF_TOTAL_APRIL }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + APRIL_CHANGES.forEach(change => { + showTooltipForCircleInSeries("#88BF4D"); + if (change === null) { + testTooltipExcludesText("Compared to preivous"); + return; + } + testPairedTooltipValues("Compared to previous month", change); + }); + }); + + it("should not omit percent change the week after DST begins", () => { + setup({ question: SUM_OF_TOTAL_DST_WEEK }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + DST_WEEK_CHANGES.forEach(change => { + showTooltipForCircleInSeries("#88BF4D"); + if (change === null) { + testTooltipExcludesText("Compared to preivous"); + return; + } + testPairedTooltipValues("Compared to previous week", change); + }); + }); + + it("should not omit percent change the day after DST begins", () => { + setup({ question: SUM_OF_TOTAL_DST_DAY }).then(dashboardId => { + visitDashboard(dashboardId); + }); + + DST_DAY_CHANGES.forEach(change => { + showTooltipForCircleInSeries("#88BF4D"); + if (change === null) { + testTooltipExcludesText("Compared to preivous"); + return; + } + testPairedTooltipValues("Compared to previous day", change); + }); + }); + }); }); function setup({ question, addedSeriesQuestion }) { diff --git a/frontend/src/metabase/lib/time-dayjs.ts b/frontend/src/metabase/lib/time-dayjs.ts index ba01f2a82d1debcd9ddeae6f48a002f5c28af9c9..82d33834e12f484232a012b187791d434c0337a4 100644 --- a/frontend/src/metabase/lib/time-dayjs.ts +++ b/frontend/src/metabase/lib/time-dayjs.ts @@ -3,6 +3,31 @@ import dayjs from "dayjs"; import type { DatetimeUnit } from "metabase-types/api/query"; +const DAYLIGHT_SAVINGS_CHANGE_TOLERANCE: Record<string, number> = { + minute: 0, + hour: 0, + // It's not possible to have two consecutive hours or minutes across the + // daylight savings time change. Daylight savings begins at 2AM on March 10th, + // so after 1AM, the next hour is 3AM. + day: 0.05, + week: 0.01, + month: 0.01, + quarter: 0, + year: 0, +}; + +/** + * This function is used to get a tolerance for the difference between two dates + * across the daylight savings time change, using dayjs' date.diff method. For + * example between March 10th (when daylight savings beigns) and March 11th, the + * .diff method will return + */ +export function getDaylightSavingsChangeTolerance(unit: string) { + return unit in DAYLIGHT_SAVINGS_CHANGE_TOLERANCE + ? DAYLIGHT_SAVINGS_CHANGE_TOLERANCE[unit] + : 0; +} + const TEXT_UNIT_FORMATS = { "day-of-week": (value: string) => { const day = dayjs.tz(value, "ddd").startOf("day"); diff --git a/frontend/src/metabase/visualizations/visualizations/CartesianChart/events.ts b/frontend/src/metabase/visualizations/visualizations/CartesianChart/events.ts index 257ca317eea824d53c07e7bd4cce38fe78fde534..452c54dccfea4e063e66792e6c7893f862993040 100644 --- a/frontend/src/metabase/visualizations/visualizations/CartesianChart/events.ts +++ b/frontend/src/metabase/visualizations/visualizations/CartesianChart/events.ts @@ -3,7 +3,10 @@ import _ from "underscore"; import { NULL_DISPLAY_VALUE } from "metabase/lib/constants"; import { formatChangeWithSign } from "metabase/lib/formatting"; import { getObjectKeys } from "metabase/lib/objects"; -import { parseTimestamp } from "metabase/lib/time-dayjs"; +import { + getDaylightSavingsChangeTolerance, + parseTimestamp, +} from "metabase/lib/time-dayjs"; import { checkNumber, isNotNull } from "metabase/lib/types"; import { ORIGINAL_INDEX_DATA_KEY, @@ -217,9 +220,15 @@ const getTooltipFooterData = ( ? "quarter" : chartModel.xAxisModel.interval.unit; + const dateDifference = currentDate.diff( + previousDate, + chartModel.xAxisModel.interval.unit, + true, + ); + let isOneIntervalAgo = - currentDate.diff(previousDate, chartModel.xAxisModel.interval.unit) === - chartModel.xAxisModel.interval.count; + Math.abs(dateDifference - chartModel.xAxisModel.interval.count) <= + getDaylightSavingsChangeTolerance(chartModel.xAxisModel.interval.unit); // Comparing the 2nd and 1st quarter of the year needs to be checked // specially, because there are fewer days in this period due to Feburary