fix summary graph selection persistence after xAxis switch

test-case: https://docs.google.com/document/d/1YYce6OV1qWWNOIgk1uKEX_jy7BwJQAh3T1EWWvY6KL8/edit?resourcekey=0-FbBWzqG6h_eWIg5rVQMFGg&tab=t.0#heading=h.164plq7xjf7i

Bug: 448352249
Change-Id: I95aae7c2c3b10b655e85f38f3e323b9cabae82c0
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/1075258
Commit-Queue: Sergei Rudenkov <sergeirudenkov@google.com>
Reviewed-by: Marcin Mordecki <mordeckimarcin@google.com>
diff --git a/infra-sk/modules/BUILD.bazel b/infra-sk/modules/BUILD.bazel
index 43433fb..e9cb61d 100644
--- a/infra-sk/modules/BUILD.bazel
+++ b/infra-sk/modules/BUILD.bazel
@@ -53,6 +53,8 @@
         ":object_ts_lib",
         "//:node_modules/@types/chai",
         "//:node_modules/chai",
+        "//:node_modules/lit",
+        "//infra-sk/modules/ElementSk:index_ts_lib",
     ],
 )
 
diff --git a/infra-sk/modules/test_util.ts b/infra-sk/modules/test_util.ts
index c0ceb4f..1dbf8a8 100644
--- a/infra-sk/modules/test_util.ts
+++ b/infra-sk/modules/test_util.ts
@@ -9,6 +9,8 @@
 
 import { expect } from 'chai';
 import { deepCopy } from './object';
+import { LitElement } from 'lit';
+import { ElementSk } from './ElementSk';
 
 /**
  * Takes a DOM element name (e.g. 'my-component-sk') and returns a factory
@@ -314,3 +316,22 @@
 export function setQueryString(q: string) {
   window.history.pushState(null, '', window.location.origin + window.location.pathname + q);
 }
+
+/**
+ * Waits for the next render cycle of a LitElement or ElementSk component, including browser paint.
+ * Useful for ensuring DOM updates have completed in tests.
+ *
+ * @param el The element to wait for.
+ */
+export async function waitForRender(el: ElementSk | LitElement) {
+  const domUpdate = () => new Promise((resolve) => setTimeout(resolve, 0));
+  if (!el) return;
+  await domUpdate();
+  if (el instanceof LitElement && el.updateComplete) {
+    await el.updateComplete;
+  }
+  // LitElement updates are done, now wait for browser to paint
+  await new Promise((resolve) => requestAnimationFrame(resolve));
+  // Final yield
+  await domUpdate();
+}
diff --git a/perf/modules/explore-simple-sk/BUILD.bazel b/perf/modules/explore-simple-sk/BUILD.bazel
index f9f87d5..a2500eb 100644
--- a/perf/modules/explore-simple-sk/BUILD.bazel
+++ b/perf/modules/explore-simple-sk/BUILD.bazel
@@ -80,6 +80,7 @@
     src = "explore-simple-sk_test.ts",
     deps = [
         ":explore-simple-sk",
+        "//:node_modules/@material/web",
         "//:node_modules/@types/chai",
         "//:node_modules/@types/sinon",
         "//:node_modules/chai",
@@ -89,5 +90,6 @@
         "//infra-sk/modules:test_util_ts_lib",
         "//perf/modules/dataframe:test_utils_ts_lib",
         "//perf/modules/json:index_ts_lib",
+        "//perf/modules/plot-summary-sk",
     ],
 )
diff --git a/perf/modules/explore-simple-sk/explore-simple-sk.ts b/perf/modules/explore-simple-sk/explore-simple-sk.ts
index 78a5b04..27a7e6c 100644
--- a/perf/modules/explore-simple-sk/explore-simple-sk.ts
+++ b/perf/modules/explore-simple-sk/explore-simple-sk.ts
@@ -140,6 +140,9 @@
 import { FavoritesDialogSk } from '../favorites-dialog-sk/favorites-dialog-sk';
 import { CommitLinks } from '../point-links-sk/point-links-sk';
 
+const DOMAIN_DATE = 'date';
+const DOMAIN_COMMIT = 'commit';
+
 /** The type of trace we are adding to a plot. */
 type addPlotType = 'query' | 'formula' | 'pivot';
 
@@ -320,7 +323,7 @@
 
   selected: PointSelected = defaultPointSelected(); // The point on a trace that was clicked on.
 
-  domain: string = 'commit'; // The domain of the x-axis, either commit or date. Default commit.
+  domain: string = DOMAIN_COMMIT; // The domain of the x-axis, either commit or date. Default commit
 
   // Deprecated.
   labelMode: LabelMode = LabelMode.Date; // The label mode for the x-axis, date or commit.
@@ -1023,7 +1026,11 @@
     return colHeader;
   }
 
-  private getCommitIndex(value: number, type: string = 'commit', max: boolean = false): number {
+  private getCommitIndex(
+    value: number,
+    type: string = DOMAIN_COMMIT,
+    max: boolean = false
+  ): number {
     // Ensure the index value is an integer.
     value = Math.round(value);
     const header = this.dfRepo.value?.header;
@@ -1031,7 +1038,7 @@
       return -1;
     }
 
-    const prop = type === 'commit' ? 'offset' : 'timestamp';
+    const prop = type === DOMAIN_COMMIT ? 'offset' : 'timestamp';
 
     // First, try to find an exact match.
     let index = header.findIndex((h) => h?.[prop] === value);
@@ -1356,25 +1363,18 @@
   // If the switch is selected, set the x-axis to date mode.
   switchXAxis(target: MdSwitch | null) {
     if (!target) return;
-    const newDomain = target.selected ? 'date' : 'commit';
-    if (this._state.domain !== newDomain) {
-      if (target.selected) {
-        this._state.labelMode = LabelMode.Date;
-        this._state.domain = 'date';
-      } else {
-        this._state.labelMode = LabelMode.CommitPosition;
-        this._state.domain = 'commit';
-      }
+    const newDomain = target.selected ? DOMAIN_DATE : DOMAIN_COMMIT;
 
-      if (this.is_chart_split) {
-        const detail = {
-          index: this.state.graph_index,
-          domain: this.state.domain,
-        };
-        this.dispatchEvent(new CustomEvent('x-axis-toggled', { detail, bubbles: true }));
-      }
-      this.updateXAxis(this._state.domain);
+    // Dispatch the event first to sync other charts.
+    if (this.is_chart_split) {
+      const detail = {
+        index: this.state.graph_index,
+        domain: newDomain,
+      };
+      this.dispatchEvent(new CustomEvent('x-axis-toggled', { detail, bubbles: true }));
     }
+    // Then update the current chart.
+    this.updateXAxis(newDomain);
   }
 
   // Set the x-axis domain to either commit or date.
@@ -1382,10 +1382,17 @@
   // It also is an entry point from multichart to update related charts which is
   // why it has a separate method.
   updateXAxis(domain: string) {
-    if (this.state.domain !== domain) {
-      this.xAxisSwitch = !this.xAxisSwitch;
-      this._state.domain = domain;
+    if (this.state.domain === domain) {
+      return;
     }
+
+    this._updateSelectionRangeForDomainChange(domain);
+
+    // Now, update the state and the child components' domains.
+    this.xAxisSwitch = !this.xAxisSwitch;
+    this._state.domain = domain;
+    this._state.labelMode = domain === DOMAIN_DATE ? LabelMode.Date : LabelMode.CommitPosition;
+
     if (this.googleChartPlot.value) {
       this.googleChartPlot.value.domain = domain as 'commit' | 'date';
     }
@@ -1397,6 +1404,34 @@
     this._stateHasChanged();
   }
 
+  private _updateSelectionRangeForDomainChange(newDomain: string) {
+    const header = this.dfRepo.value?.header;
+    const plotSummary = this.plotSummary.value;
+    const oldDomain = this.state.domain;
+
+    if (header && plotSummary && plotSummary.selectedValueRange) {
+      const currentRange = plotSummary.selectedValueRange;
+
+      const beginIndex = this.getCommitIndex(currentRange.begin, oldDomain);
+      const endIndex = this.getCommitIndex(currentRange.end, oldDomain, true);
+
+      if (beginIndex !== -1 && endIndex !== -1) {
+        const newBegin =
+          newDomain === DOMAIN_DATE ? header[beginIndex]!.timestamp : header[beginIndex]!.offset;
+        const newEnd =
+          newDomain === DOMAIN_DATE ? header[endIndex]!.timestamp : header[endIndex]!.offset;
+        const newRange = { begin: newBegin, end: newEnd };
+
+        // Set the cached value on the child. It will apply this selection once
+        // its internal chart has re-rendered with the new domain.
+        (plotSummary as any).cachedSelectedValueRange = newRange;
+        if (this.googleChartPlot.value) {
+          this.googleChartPlot.value.selectedValueRange = newRange;
+        }
+      }
+    }
+  }
+
   // updates the chart height using a string input.
   // typically 500px for a single chart and 250px for multiple charts
   updateChartHeight(height: string) {
@@ -1906,7 +1941,7 @@
 
       let beginValue: number = beginCommit!.offset;
       let endValue: number = endCommit!.offset;
-      if (this.state.domain === 'date') {
+      if (this.state.domain === DOMAIN_DATE) {
         beginValue = beginCommit!.timestamp;
         endValue = endCommit!.timestamp;
       }
diff --git a/perf/modules/explore-simple-sk/explore-simple-sk_test.ts b/perf/modules/explore-simple-sk/explore-simple-sk_test.ts
index 59e818d..b407608 100644
--- a/perf/modules/explore-simple-sk/explore-simple-sk_test.ts
+++ b/perf/modules/explore-simple-sk/explore-simple-sk_test.ts
@@ -9,6 +9,8 @@
   TimestampSeconds,
   Trace,
   TraceSet,
+  DataFrame,
+  ReadOnlyParamSet,
 } from '../json';
 import { deepCopy } from '../../../infra-sk/modules/object';
 import {
@@ -23,15 +25,53 @@
   updateShortcut,
   State,
 } from './explore-simple-sk';
-import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util';
+import { MdDialog } from '@material/web/dialog/dialog';
+import { MdSwitch } from '@material/web/switch/switch';
+import { PlotSummarySk } from '../plot-summary-sk/plot-summary-sk';
+import { setUpElementUnderTest, waitForRender } from '../../../infra-sk/modules/test_util';
 import { generateFullDataFrame } from '../dataframe/test_utils';
 import sinon from 'sinon';
+// Import for side effects. Make `plotSummary`(has no direct interaction with the module)
+// work when run in isolation.
+import './explore-simple-sk';
 
 fetchMock.config.overwriteRoutes = true;
 
 const now = 1726081856; // an arbitrary UNIX time;
 const timeSpan = 89; // an arbitrary prime number for time span between commits .
 
+window.perf = {
+  instance_url: '',
+  radius: 2,
+  key_order: null,
+  num_shift: 50,
+  interesting: 2,
+  step_up_only: false,
+  commit_range_url: '',
+  demo: true,
+  display_group_by: false,
+  hide_list_of_commits_on_explore: false,
+  notifications: 'none',
+  fetch_chrome_perf_anomalies: false,
+  feedback_url: '',
+  chat_url: '',
+  help_url_override: '',
+  trace_format: '',
+  need_alert_action: false,
+  bug_host_url: '',
+  git_repo_url: '',
+  keys_for_commit_range: [],
+  keys_for_useful_links: [],
+  skip_commit_detail_display: false,
+  image_tag: 'fake-tag',
+  remove_default_stat_value: false,
+  enable_skia_bridge_aggregation: false,
+  show_json_file_display: false,
+  always_show_commit_info: false,
+  show_triage_link: true,
+  show_bisect_btn: true,
+};
+
 describe('calculateRangeChange', () => {
   const offsets: CommitRange = [100, 120] as CommitRange;
 
@@ -93,45 +133,6 @@
   });
 });
 
-// this function is needed to support other unit tests
-describe('applyFuncToTraces', () => {
-  window.perf = {
-    instance_url: '',
-    radius: 2,
-    key_order: null,
-    num_shift: 50,
-    interesting: 2,
-    step_up_only: false,
-    commit_range_url: '',
-    demo: true,
-    display_group_by: false,
-    hide_list_of_commits_on_explore: false,
-    notifications: 'none',
-    fetch_chrome_perf_anomalies: false,
-    feedback_url: '',
-    chat_url: '',
-    help_url_override: '',
-    trace_format: '',
-    need_alert_action: false,
-    bug_host_url: '',
-    git_repo_url: '',
-    keys_for_commit_range: [],
-    keys_for_useful_links: [],
-    skip_commit_detail_display: false,
-    image_tag: 'fake-tag',
-    remove_default_stat_value: false,
-    enable_skia_bridge_aggregation: false,
-    show_json_file_display: false,
-    always_show_commit_info: false,
-    show_triage_link: true,
-    show_bisect_btn: true,
-  };
-
-  // Create a common element-sk to be used by all the tests.
-  const explore = document.createElement('explore-simple-sk') as ExploreSimpleSk;
-  document.body.appendChild(explore);
-});
-
 describe('PointSelected', () => {
   it('defaults to not having a name', () => {
     const p = defaultPointSelected();
@@ -698,3 +699,212 @@
     );
   });
 });
+
+describe('x-axis domain switching', () => {
+  const INITIAL_TIMESTAMP_BEGIN = 1672531200;
+  const INITIAL_TIMESTAMP_END = 1672542000;
+  const COMMIT_101 = 101;
+  const COMMIT_102 = 102;
+  const TIMESTAMP_101 = 1672534800;
+  const TIMESTAMP_102 = 1672538400;
+  const ROUNDING_TOLERANCE_SECONDS = 120;
+  // A simple header for converting between commit offsets and timestamps.
+  const testHeader: ColumnHeader[] = [
+    {
+      offset: CommitNumber(100),
+      timestamp: TimestampSeconds(1672531200),
+      hash: 'h1',
+      author: '',
+      message: '',
+      url: '',
+    },
+    {
+      offset: CommitNumber(101),
+      timestamp: TimestampSeconds(1672534800),
+      hash: 'h2',
+      author: '',
+      message: '',
+      url: '',
+    },
+    {
+      offset: CommitNumber(102),
+      timestamp: TimestampSeconds(1672538400),
+      hash: 'h3',
+      author: '',
+      message: '',
+      url: '',
+    },
+    {
+      offset: CommitNumber(103),
+      timestamp: TimestampSeconds(1672542000),
+      hash: 'h4',
+      author: '',
+      message: '',
+      url: '',
+    },
+  ];
+
+  const testDataFrame: DataFrame = {
+    traceset: TraceSet({
+      ',config=test,': Trace([1, 2, 3, 4]),
+    }),
+    header: testHeader,
+    paramset: {} as ReadOnlyParamSet,
+    skip: 0,
+    traceMetadata: [],
+  };
+
+  const testFrameResponse: FrameResponse = {
+    dataframe: testDataFrame,
+    anomalymap: null,
+    display_mode: 'display_plot',
+    skps: [],
+    msg: '',
+  };
+
+  let explore: ExploreSimpleSk;
+
+  beforeEach(async () => {
+    explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')();
+    await window.customElements.whenDefined('explore-simple-sk');
+    await window.customElements.whenDefined('dataframe-repository-sk');
+    await window.customElements.whenDefined('plot-summary-sk');
+  });
+
+  // Helper function to set up the component for domain switching tests.
+  async function setupDomainSwitchTest(domain: 'date' | 'commit'): Promise<PlotSummarySk> {
+    explore.state = {
+      ...explore.state,
+      queries: ['config=test'],
+      domain: domain,
+      plotSummary: true,
+      begin: INITIAL_TIMESTAMP_BEGIN,
+      end: INITIAL_TIMESTAMP_END,
+      requestType: 0,
+    };
+    await waitForRender(explore);
+
+    // Provide data to the component.
+    await explore.UpdateWithFrameResponse(
+      testFrameResponse,
+      {
+        begin: explore.state.begin,
+        end: explore.state.end,
+        num_commits: 250,
+        request_type: 0,
+        formulas: [],
+        queries: ['config=test'],
+        keys: '',
+        tz: 'UTC',
+        pivot: null,
+        disable_filter_parent_traces: false,
+      },
+      false,
+      null,
+      false
+    );
+    await waitForRender(explore);
+
+    await new Promise((resolve) => setTimeout(resolve, 0));
+
+    const plotSummary = explore.querySelector('plot-summary-sk') as PlotSummarySk;
+    assert.exists(plotSummary, 'The plot-summary-sk element should be in the DOM.');
+    return plotSummary;
+  }
+
+  it('preserves selection when switching from date to commit', async () => {
+    const plotSummary = await setupDomainSwitchTest('date');
+
+    // Set an initial time-based selection on plot-summary-sk
+    const initialSelection = { begin: TIMESTAMP_101, end: TIMESTAMP_102 };
+    plotSummary.selectedValueRange = initialSelection;
+    await plotSummary.updateComplete;
+    await new Promise((resolve) => setTimeout(resolve, 0));
+
+    const settingsDialog = explore.querySelector('#settings-dialog') as MdDialog;
+    const switchEl = settingsDialog.querySelector('#commit-switch') as MdSwitch;
+    assert.exists(switchEl, '#commit-switch element not found.');
+
+    // Simulate switching to 'commit' domain (selected = false)
+    switchEl!.selected = false;
+    switchEl!.dispatchEvent(new Event('change'));
+
+    // Wait for ExploreSimpleSk to handle the change and update its children
+    await waitForRender(explore);
+    await plotSummary.updateComplete;
+    await new Promise((resolve) => setTimeout(resolve, 100));
+
+    // rare, but can be flaky here. Since it wait for async event.
+    // Increase of timeout above can help.
+    assert.exists(
+      plotSummary.selectedValueRange,
+      'selectedValueRange should not be null after switch'
+    );
+
+    // Although the actual 'begin' and 'end' values are integers, they can be converted to
+    // floating-point numbers for UI rendering to prevent the graph from "jumping" when the x-axis
+    // domain is switched. The approximation in this test is used solely to prevent failures caused
+    // by floating-point arithmetic inaccuracies, e.g., 101 !== 101.000000001.
+    assert.approximately(
+      plotSummary.selectedValueRange.begin as number,
+      COMMIT_101,
+      1e-3,
+      'Selected range.begin did not convert correctly'
+    );
+
+    assert.approximately(
+      plotSummary.selectedValueRange.end as number,
+      COMMIT_102,
+      1e-3,
+      'Selected range.end did not convert correctly'
+    );
+
+    assert.equal(explore.state.domain, 'commit', 'Explore state domain should be commit');
+    assert.equal(plotSummary.domain, 'commit', 'PlotSummary domain property should be commit');
+  });
+
+  it('preserves selection when switching from commit to date', async () => {
+    const roundingToleranceSeconds = ROUNDING_TOLERANCE_SECONDS;
+    const plotSummary = await setupDomainSwitchTest('commit');
+
+    // Set an initial commit-based selection on plot-summary-sk
+    const initialSelection = { begin: COMMIT_101, end: COMMIT_102 };
+    plotSummary.selectedValueRange = initialSelection;
+    await plotSummary.updateComplete;
+    await new Promise((resolve) => setTimeout(resolve, 0));
+
+    const settingsDialog = explore.querySelector('#settings-dialog') as MdDialog;
+    const switchEl = settingsDialog.querySelector('#commit-switch') as MdSwitch;
+    assert.exists(switchEl, '#commit-switch element not found.');
+
+    // Simulate switching to 'date' domain (selected = true)
+    switchEl!.selected = true;
+    switchEl!.dispatchEvent(new Event('change'));
+
+    await waitForRender(explore);
+    await plotSummary.updateComplete;
+    await new Promise((resolve) => setTimeout(resolve, 100));
+
+    assert.exists(
+      plotSummary.selectedValueRange,
+      'selectedValueRange should not be null after switch'
+    );
+
+    assert.approximately(
+      plotSummary.selectedValueRange.begin as number,
+      TIMESTAMP_101,
+      roundingToleranceSeconds,
+      'Selected range.begin did not convert correctly'
+    );
+
+    assert.approximately(
+      plotSummary.selectedValueRange.end as number,
+      TIMESTAMP_102,
+      roundingToleranceSeconds,
+      'Selected range.end did not convert correctly'
+    );
+
+    assert.equal(explore.state.domain, 'date', 'Explore state domain should be date');
+    assert.equal(plotSummary.domain, 'date', 'PlotSummary domain property should be date');
+  });
+});