Repository: elastic/elastic-charts
Pull request #2647: feat(interactions): add modifier keys pressed information available in the `onElementClick` and `onBrushEnd` listeners
Author: login: mariairiartef
Labels: name: enhancement
description: New feature or request
name: :interactions
description: Interactions related issue
name: :xy
description: Bar/Line/Area chart related
name: :heatmap
description: Heatmap/Swimlane chart related issue
name: :all
description: Applies to all chart types
Created: 2025-04-11T13:41:24Z | Merged: 2025-05-07T12:32:23Z

## Summary

Adds information about the **modifier keys pressed** during interactions, making it available to the `onElementClick` and `onBrushEnd` listeners in **XY** and **Heatmap** charts.

### Details

1. **Update Redux actions**
Add the `keyPressed` property to the Redux actions for mouse events (onMouseDown, onMouseUp, onPointerMove)
2. **Update Redux state**
Add a `keyPressed` property to the `PointerStates` in the Redux state.
Ensure this property is updated with the current modifier keys (metaKey, ctrlKey, etc.) during mouse events.
3. **Handle state updates in reducers**
Modify the `handleMouseActions` reducer to update the `keyPressed` property in the state.
4. **Pass `keyPressed` to selectors**
Update `createOnBrushEndCaller` and `createOnClickCaller` to include the `keyPressed` property when firing events
5. **Update event listeners**
Pass the `keyPressed` property to the `onElementClick` and `onBrushEnd` listeners.

<!--
  Summarize your PR. This will be included in our newsletter

  - The summary is intended for a consumer audience, avoid any internal or implementation details. You can include those in the details section.
  - Generally only `fix:` and `feat:` PRs will be included in the newsletter. Also, PRs with BREAKING CHANGES are added.
  - Describe the feature or fix as you would if you were advertising it in the newsletter:
      - ❌ : This commit close the request `#123` and adds the prop `helloWorld` to `Settings`
      - ✅ : The `helloWorld` prop is now available in the `Settings` component to bring joy when rendering the chart.
      - ❌ : Fixing the tooltip position outside the chart area avoiding overflows.
      - ✅ : The tooltip no longer overflows the chart DOM container when using the `tooltip.boundary = 'chart'` in the `Settings` component.
  - Add a clear screenshot or animated gif as an example if the change can be understood better and easier with a visual aid.
  - If the PR involves a bigger feature, please add more context to it, describing why the feature was added, what actually improve, and how the users can leverage it to improve their data visualizations
  - If the PR involves a breaking change include the following part and clearly state which contract is broken:

    ### BREAKING CHANGE
    The `tooltip.boundary` prop in the `Settings` component now only accepts a single DOM element ID.
-->



<!-- screenshot/gif/mpeg-4 for visual changes -->

<!--

## Details

1. Update `PointerStates` to have the `keyPressed` information

In packages/charts/src/state/pointer_states.ts, update the PointerStates with keyPressed. This is part of the  InteractionsState in packages/charts/src/state/interactions_state.ts.

2. Update the `keyPressed` state property where actions are defined

`handleMouseActions` in `packages/charts/src/state/reducers/interactions.ts` defines:
* onPointerMove
* onMouseDown
* onMouseUp

These actions update the states. They also modify the keyPressed property with the payload.

3. Pass the `keyPressed` from the event when the actions are called

In `packages/charts/src/components/chart_container.tsx`, add shiftKey, ctrlKey, altKey and metaKey to `keyPressed`.

-->

## Issues

Closes https://github.com/elastic/elastic-charts/issues/2349?reload=1?reload=1

<!--
  Issues this pr is fixing or closing

  e.g.

  This completes a missing feature requested by APM regarding the tooltip positioning #921
  fix #1108
-->



## Checklist

<!-- Delete any items that are not applicable to this PR. -->
- [x] The proper **chart type** label has been added (e.g. `:xy`, `:partition`)
- [x] The proper **feature** labels have been added (e.g. `:interactions`, `:axis`)
- [x] All related issues have been linked (i.e. `closes #123`, `fixes #123`)
- [ ] New public API exports have been added to `packages/charts/src/index.ts`
- [x] Unit tests have been added or updated to match the most common scenarios
- [ ] The proper documentation and/or storybook story has been added or updated
- [ ] The code has been checked for cross-browser compatibility (Chrome, Firefox, Safari, Edge)


Linked issue #1108: [Heatmap] the tooltip callout box is centered at the mouse pointer
**Describe the bug**
It looks like it's partly a bug (vertical centering) and partly, a less than optimal user experience, or issue, depending on circumstances (horizontal snap with some offset, which causes the occlusion of the very hovered thing):

![image](https://user-images.githubusercontent.com/1548516/113742323-ce315380-9702-11eb-8cb2-d0fcec69932f.png)
The cursor is not visible on the screenshot; it's over Deborah / 1965.

**To Reproduce**
Steps to reproduce the behavior:
1. Go to https://elastic.github.io/elastic-charts/?path=/story/heatmap-alpha--categorical
2. Hover over cells, at various window widths

**Expected behaviour**
I think that ensuring that the tooltip box is always to the right and below (or maybe above) of the mouse pointer, or even the cell (in this case, with a callout arrow pointing to the cell) would be better

**Checklist**

Delete any items that are not applicable to this issue.

- [ ] every related Kibana issue is listed under `Kibana Cross Issues` list
- [ ] `kibana cross issue` tag is associated to the issue if any kibana cross issue is present

Linked issue comments: user: login: markov00
created_at: 2021-04-07T12:46:29Z
body: hei @darnautov are aware of this issue? 
author_association: COLLABORATOR
user: login: darnautov
created_at: 2021-04-07T14:22:54Z
body: thanks for creating an issue, it is not the case with another [story](https://elastic.github.io/elastic-charts/?path=/story/heatmap-alpha--basic) tho. I'll work on a fix 👍 

author_association: CONTRIBUTOR
user: login: markov00
created_at: 2021-05-26T14:09:38Z
body: This will be fixed in #1163 @darnautov you can see the fix here: https://github.com/elastic/elastic-charts/pull/1163/files#diff-d3b5933a97ba501883dea0379024b3a27b0afbe64480e581cf915c1fd0ef5660
instead of using the center of the cell as anchor point for the tooltip:
```
x1: firstShape.x + chartDimensions.left + firstShape.width / 2,
```
I've changed that to anchor the tooltip around the cell shape:
```
x: firstShape.x + chartDimensions.left,
width: firstShape.width,
y: firstShape.y - chartDimensions.top,
height: firstShape.height,
```
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-06-04T16:03:59Z
body: :tada: This issue has been resolved in version 30.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v30.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/30.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-06-29T04:28:37Z
body: :tada: This issue has been resolved in version 31.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v31.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/31.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-12T15:16:03Z
body: :tada: This issue has been resolved in version 32.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v32.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/32.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-14T15:08:13Z
body: :tada: This issue has been resolved in version 33.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v33.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/33.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-16T16:34:48Z
body: :tada: This issue has been resolved in version 33.0.1 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v33.0.1)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/33.0.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-20T08:40:14Z
body: :tada: This issue has been resolved in version 32.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v32.0.0)
- [npm package (@release-32.x dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/32.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-20T10:14:49Z
body: :tada: This issue has been resolved in version 33.0.2 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v33.0.2)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/33.0.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-07-28T16:49:32Z
body: :tada: This issue has been resolved in version 33.1.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v33.1.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/33.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-08-09T15:55:16Z
body: :tada: This issue has been resolved in version 33.2.1 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v33.2.1)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/33.2.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-08-16T23:12:18Z
body: :tada: This issue has been resolved in version 34.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v34.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/34.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-08-19T18:48:29Z
body: :tada: This issue has been resolved in version 34.1.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v34.1.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/34.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-08-23T14:52:41Z
body: :tada: This issue has been resolved in version 34.2.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v34.2.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/34.2.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-08-25T09:37:30Z
body: :tada: This issue has been resolved in version 34.2.1 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v34.2.1)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/34.2.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-09-13T15:58:36Z
body: :tada: This issue has been resolved in version 35.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v35.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/35.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-10-05T10:16:59Z
body: :tada: This issue has been resolved in version 37.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v37.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/37.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-10-15T14:01:31Z
body: :tada: This issue has been resolved in version 38.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-10-26T14:40:26Z
body: :tada: This issue has been resolved in version 38.1.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.1.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-10-28T12:11:20Z
body: :tada: This issue has been resolved in version 38.1.1 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.1.1)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.1.1)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-10-29T10:39:34Z
body: :tada: This issue has been resolved in version 38.1.2 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.1.2)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.1.2)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-11-01T10:10:12Z
body: :tada: This issue has been resolved in version 38.1.3 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.1.3)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.1.3)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-11-09T08:30:06Z
body: :tada: This issue has been resolved in version 39.0.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v39.0.0)
- [npm package (@latest dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/39.0.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR
user: login: nickofthyme
created_at: 2021-12-13T15:33:38Z
body: :tada: This issue has been resolved in version 38.1.0 :tada:

The release is available on:
- [GitHub release](https://github.com/elastic/elastic-charts/releases/tag/v38.1.0)
- [npm package (@release-38.1.x dist-tag)](https://www.npmjs.com/package/@elastic/charts/v/38.1.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR

Diff:
diff --git a/packages/charts/api/charts.api.md b/packages/charts/api/charts.api.md
index 4bbe08adda0..abcdaba4c6f 100644
--- a/packages/charts/api/charts.api.md
+++ b/packages/charts/api/charts.api.md
@@ -422,10 +422,12 @@ export const BrushAxis: Readonly<{
 // @public (undocumented)
 export type BrushAxis = $Values<typeof BrushAxis>;
 
-// @public (undocumented)
-export type BrushEndListener = (brushAreaEvent: BrushEvent) => void;
+// @public
+export type BrushEndListener = (brushAreaEvent: BrushEvent, options?: {
+    keyPressed: KeyPressed;
+}) => void;
 
-// @public (undocumented)
+// @public
 export type BrushEvent = XYBrushEvent | HeatmapBrushEvent;
 
 // Warning: (ae-forgotten-export) The symbol "buildProps_5" needs to be exported by the entry point index.d.ts
@@ -1150,8 +1152,10 @@ export interface DomainRange {
     minInterval?: number;
 }
 
-// @public (undocumented)
-export type ElementClickListener = (elements: Array<XYChartElementEvent | PartitionElementEvent | FlameElementEvent | HeatmapElementEvent | WordCloudElementEvent | MetricElementEvent>) => void;
+// @public
+export type ElementClickListener = (elements: Array<XYChartElementEvent | PartitionElementEvent | FlameElementEvent | HeatmapElementEvent | WordCloudElementEvent | MetricElementEvent>, options?: {
+    keyPressed: KeyPressed;
+}) => void;
 
 // @public (undocumented)
 export type ElementOverListener = (elements: Array<XYChartElementEvent | PartitionElementEvent | FlameElementEvent | HeatmapElementEvent | WordCloudElementEvent | MetricElementEvent>) => void;
@@ -1237,7 +1241,7 @@ export type FitConfig = {
 // @public
 export const Flame: <D extends BaseDatum = any>(props: SFProps<FlameSpec<D>, keyof (typeof buildProps_11)["overrides"], keyof (typeof buildProps_11)["defaults"], keyof (typeof buildProps_11)["optionals"], keyof (typeof buildProps_11)["requires"]>) => null;
 
-// @public (undocumented)
+// @public
 export type FlameElementEvent = FlameLayerValue;
 
 // @public
@@ -1458,7 +1462,7 @@ export interface GridLineStyle {
     visible: boolean;
 }
 
-// @public (undocumented)
+// @public
 export interface GroupBrushExtent {
     // (undocumented)
     extent: [number, number];
@@ -1514,7 +1518,7 @@ export interface HeatmapBandsColorScale {
     type: 'bands';
 }
 
-// @public (undocumented)
+// @public
 export interface HeatmapBrushEvent extends SmallMultiplesDatum {
     // (undocumented)
     cells: Cell[];
@@ -1536,7 +1540,7 @@ export interface HeatmapCellDatum extends SmallMultiplesDatum {
     y: NonNullable<PrimitiveValue>;
 }
 
-// @public (undocumented)
+// @public
 export type HeatmapElementEvent = [cell: Cell, seriesIdentifier: SeriesIdentifier];
 
 // @public (undocumented)
@@ -1736,6 +1740,9 @@ export type IsUnknown<T, True, False = never> = unknown extends T ? IsAny<T, Fal
 // @public (undocumented)
 export type Key = CategoryKey;
 
+// @public
+export type KeyPressed = Record<ModifierKeys, boolean>;
+
 // @public (undocumented)
 export type LabelAccessor<T = PrimitiveValue> = (value: T) => string;
 
@@ -2158,6 +2165,9 @@ export type MetricWTrend = (MetricWNumber | MetricWText | MetricWNumberArrayValu
 // @public (undocumented)
 export const MODEL_KEY = "parent";
 
+// @public
+export type ModifierKeys = 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey';
+
 // @public
 export type Neverify<T extends Record<string, unknown>> = {
     [Key in keyof T]?: never;
@@ -2236,7 +2246,7 @@ export type PartialTheme = RecursivePartial<Theme>;
 // @public
 export const Partition: <D extends BaseDatum = any>(props: SFProps<PartitionSpec<D>, keyof (typeof buildProps)["overrides"], keyof (typeof buildProps)["defaults"], keyof (typeof buildProps)["optionals"], keyof (typeof buildProps)["requires"]>) => null;
 
-// @public (undocumented)
+// @public
 export type PartitionElementEvent = [layers: Array<LayerValue>, seriesIdentifier: SeriesIdentifier];
 
 // Warning: (ae-forgotten-export) The symbol "LabelConfig" needs to be exported by the entry point index.d.ts
@@ -3471,7 +3481,7 @@ export type WillRenderListener = () => void;
 // @alpha
 export const Wordcloud: FC<SFProps<WordcloudSpec, "chartType" | "specType", "data" | "fontStyle" | "fontWeight" | "fontFamily" | "padding" | "minFontSize" | "maxFontSize" | "exponent" | "startAngle" | "endAngle" | "angleCount" | "spiral" | "weightFn" | "outOfRoomCallback", never, "id">>;
 
-// @public (undocumented)
+// @public
 export type WordCloudElementEvent = [model: WordModel, seriesIdentifier: SeriesIdentifier];
 
 // Warning: (ae-incompatible-release-tags) The symbol "WordcloudProps" is marked as @public, but its signature references "Wordcloud" which is marked as @alpha
@@ -3532,7 +3542,7 @@ export interface WordModel {
 // @public (undocumented)
 export type XScaleType = typeof ScaleType.Ordinal | ScaleContinuousType;
 
-// @public (undocumented)
+// @public
 export interface XYBrushEvent {
     // (undocumented)
     x?: [number, number];
@@ -3540,7 +3550,7 @@ export interface XYBrushEvent {
     y?: Array<GroupBrushExtent>;
 }
 
-// @public (undocumented)
+// @public
 export type XYChartElementEvent = [geometry: GeometryValue, seriesIdentifier: XYChartSeriesIdentifier];
 
 // @public (undocumented)
diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/key_modifiers.test.ts b/packages/charts/src/chart_types/heatmap/state/selectors/key_modifiers.test.ts
new file mode 100644
index 00000000000..daa67808ebb
--- /dev/null
+++ b/packages/charts/src/chart_types/heatmap/state/selectors/key_modifiers.test.ts
@@ -0,0 +1,100 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Store } from 'redux';
+
+import { createOnBrushEndCaller } from './on_brush_end_caller';
+import { createOnElementClickCaller } from './on_element_click_caller';
+import { simulateBrushSequence, simulateClickSequence } from '../../../../mocks/interactions';
+import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs';
+import { createMockBrushEndListener, MockStore } from '../../../../mocks/store';
+import { ScaleType } from '../../../../scales/constants';
+import type { GlobalChartState } from '../../../../state/chart_state';
+import { getModifierKeys, type KeyPressed, noModifierKeysPressed } from '../../../../utils/keys';
+
+const metaKeyPressed = getModifierKeys({ metaKey: true });
+
+const hearmapSeriesSpec = MockSeriesSpec.heatmap({
+  xScale: { type: ScaleType.Ordinal },
+  data: [
+    { x: 'a', y: 'ya', value: 1 },
+    { x: 'b', y: 'ya', value: 2 },
+    { x: 'c', y: 'ya', value: 3 },
+    { x: 'a', y: 'yb', value: 4 },
+    { x: 'b', y: 'yb', value: 5 },
+    { x: 'c', y: 'yb', value: 6 },
+    { x: 'a', y: 'yc', value: 7 },
+    { x: 'b', y: 'yc', value: 8 },
+    { x: 'c', y: 'yc', value: 9 },
+  ],
+});
+
+describe('Key Modifiers on BrushEnd', () => {
+  let store: Store<GlobalChartState>;
+  let mockBrushEndListener: ReturnType<typeof createMockBrushEndListener>;
+
+  beforeEach(() => {
+    store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
+    mockBrushEndListener = createMockBrushEndListener();
+    const mockSettingsSpec = MockGlobalSpec.settingsNoMargins({ onBrushEnd: mockBrushEndListener });
+    MockStore.addSpecs([mockSettingsSpec, hearmapSeriesSpec], store);
+
+    const onBrushCaller = createOnBrushEndCaller();
+    store.subscribe(() => {
+      onBrushCaller(store.getState());
+    });
+  });
+
+  it('should call the brush end listener with no modifier keys pressed when keyPressed is not explicitly passed', () => {
+    simulateBrushSequence({ store });
+    verifyListenerCall(mockBrushEndListener, noModifierKeysPressed);
+  });
+
+  it('should call the brush end listener with the correct modifier keys', () => {
+    simulateBrushSequence({ store, keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockBrushEndListener, metaKeyPressed);
+
+    simulateBrushSequence({ store, time: [300, 400, 500], keysOnMouseDown: noModifierKeysPressed });
+    verifyListenerCall(mockBrushEndListener, noModifierKeysPressed, 2);
+  });
+});
+
+describe('Key Modifiers on ElementClick', () => {
+  let store: Store<GlobalChartState>;
+  let mockOnElementClick: jest.Mock<void, any[]>;
+
+  beforeEach(() => {
+    store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
+    mockOnElementClick = jest.fn<void, any[]>((): void => undefined);
+    const mockSettingsSpec = MockGlobalSpec.settingsNoMargins({ onElementClick: mockOnElementClick });
+    MockStore.addSpecs([mockSettingsSpec, hearmapSeriesSpec], store);
+
+    const onElementClickCaller = createOnElementClickCaller();
+    store.subscribe(() => {
+      onElementClickCaller(store.getState());
+    });
+  });
+
+  it('should call the element click listener with no modifier keys pressed when keyPressed is not explicitly passed', () => {
+    simulateClickSequence({ store });
+    verifyListenerCall(mockOnElementClick, noModifierKeysPressed);
+  });
+
+  it('should call the element click listener with the correct modifier keys', () => {
+    simulateClickSequence({ store, keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockOnElementClick, metaKeyPressed);
+
+    simulateClickSequence({ store, time: [300, 400, 500], keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockOnElementClick, metaKeyPressed, 2);
+  });
+});
+
+function verifyListenerCall(mockListener: jest.Mock, expectedKeyPressed: KeyPressed, numberOfCalls: number = 1) {
+  expect(mockListener).toHaveBeenCalledTimes(numberOfCalls);
+  expect(mockListener).toHaveBeenNthCalledWith(numberOfCalls, expect.anything(), { keyPressed: expectedKeyPressed });
+}
diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts b/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts
index 795119374b5..950097c8409 100644
--- a/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts
+++ b/packages/charts/src/chart_types/heatmap/state/selectors/on_brush_end_caller.ts
@@ -15,10 +15,12 @@ import type { HeatmapBrushEvent, SettingsSpec } from '../../../../specs/settings
 import type { GlobalChartState } from '../../../../state/chart_state';
 import { createCustomCachedSelector } from '../../../../state/create_selector';
 import type { DragState } from '../../../../state/pointer_states';
+import { getKeyPressedSelector } from '../../../../state/selectors/get_key_pressed';
 import { getLastDragSelector } from '../../../../state/selectors/get_last_drag';
 import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
 import type { DragCheckProps } from '../../../../utils/events';
 import { hasDragged } from '../../../../utils/events';
+import type { KeyPressed } from '../../../../utils/keys';
 
 /**
  * Will call the onBrushEnd listener every time the following preconditions are met:
@@ -31,7 +33,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
   let selector: OutputSelector<
     Array<(state: GlobalChartState) => GlobalChartState>,
     void,
-    (res1: DragState | null, res3: SettingsSpec, res4: HeatmapBrushEvent | null) => void
+    (res1: DragState | null, res2: SettingsSpec, res3: HeatmapBrushEvent | null, res4: KeyPressed) => void
   > | null = null;
 
   return (state) => {
@@ -42,8 +44,8 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
         return;
       }
       selector = createCustomCachedSelector(
-        [getLastDragSelector, getSettingsSpecSelector, getPickedCells],
-        (lastDrag, { onBrushEnd }, pickedCells): void => {
+        [getLastDragSelector, getSettingsSpecSelector, getPickedCells, getKeyPressedSelector],
+        (lastDrag, { onBrushEnd }, pickedCells, keyPressed): void => {
           const nextProps: DragCheckProps = {
             lastDrag,
             onBrushEnd,
@@ -52,7 +54,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
             return;
           }
           if (lastDrag !== null && hasDragged(prevProps, nextProps)) {
-            onBrushEnd(pickedCells);
+            onBrushEnd(pickedCells, { keyPressed });
           }
           prevProps = nextProps;
         },
diff --git a/packages/charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts b/packages/charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts
index 56e75b3cd55..7175008853a 100644
--- a/packages/charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts
+++ b/packages/charts/src/chart_types/heatmap/state/selectors/on_element_click_caller.ts
@@ -16,6 +16,7 @@ import type { SettingsSpec } from '../../../../specs';
 import type { GlobalChartState } from '../../../../state/chart_state';
 import { createCustomCachedSelector } from '../../../../state/create_selector';
 import type { PointerState } from '../../../../state/pointer_states';
+import { getKeyPressedSelector } from '../../../../state/selectors/get_key_pressed';
 import { getLastClickSelector } from '../../../../state/selectors/get_last_click';
 import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
 import { isClicking } from '../../../../state/utils/is_clicking';
@@ -35,8 +36,8 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void
   return (state: GlobalChartState) => {
     if (selector === null && state.chartType === ChartType.Heatmap) {
       selector = createCustomCachedSelector(
-        [getHeatmapSpecSelector, getLastClickSelector, getSettingsSpecSelector, getPickedShapes],
-        (spec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes): void => {
+        [getHeatmapSpecSelector, getLastClickSelector, getSettingsSpecSelector, getPickedShapes, getKeyPressedSelector],
+        (spec, lastClick: PointerState | null, settings: SettingsSpec, pickedShapes, keyPressed): void => {
           if (!spec) {
             return;
           }
@@ -55,7 +56,7 @@ export function createOnElementClickCaller(): (state: GlobalChartState) => void
                 key: `spec{${spec.id}}`,
               },
             ]);
-            settings.onElementClick(elements);
+            settings.onElementClick(elements, { keyPressed });
           }
           prevClick = lastClick;
         },
diff --git a/packages/charts/src/chart_types/xy_chart/state/chart_state.interactions.test.tsx b/packages/charts/src/chart_types/xy_chart/state/chart_state.interactions.test.tsx
index 9f9dec56362..35ff3b24462 100644
--- a/packages/charts/src/chart_types/xy_chart/state/chart_state.interactions.test.tsx
+++ b/packages/charts/src/chart_types/xy_chart/state/chart_state.interactions.test.tsx
@@ -28,9 +28,9 @@ import { ChartType } from '../..';
 import { Icon } from '../../../components/icons/icon';
 import type { Rect } from '../../../geoms/types';
 import { MockAnnotationSpec, MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs';
-import { MockStore } from '../../../mocks/store';
+import { createMockBrushEndListener, MockStore } from '../../../mocks/store';
 import { ScaleType } from '../../../scales/constants';
-import type { BrushEvent, SettingsSpec } from '../../../specs';
+import type { SettingsSpec } from '../../../specs';
 import { BrushAxis, TooltipType } from '../../../specs';
 import { SpecType } from '../../../specs/spec_type'; // kept as long-winded import on separate line otherwise import circularity emerges
 import { onExternalPointerEvent } from '../../../state/actions/events';
@@ -39,6 +39,7 @@ import type { GlobalChartState } from '../../../state/chart_state';
 import { getSettingsSpecSelector } from '../../../state/selectors/get_settings_spec';
 import type { RecursivePartial } from '../../../utils/common';
 import { Position } from '../../../utils/common';
+import { noModifierKeysPressed } from '../../../utils/keys';
 import type { AxisStyle } from '../../../utils/themes/theme';
 import type { BarSeriesSpec, BasicSeriesSpec, AxisSpec } from '../utils/specs';
 import { StackMode, SeriesType, AnnotationDomainType } from '../utils/specs';
@@ -240,14 +241,24 @@ describe('Chart state pointer interactions', () => {
     });
 
     it('should avoid calling pointer update listener if moving over the same element', () => {
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 10, y: chartTop + 10 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 10, y: chartTop + 10 },
+          time: 0,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
 
       const tooltipInfo1 = getHighlightedTooltipTooltipValuesSelector(store.getState());
       expect(tooltipInfo1.tooltip.values.length).toBe(1);
       // avoid calls
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 12, y: chartTop + 12 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 12, y: chartTop + 12 },
+          time: 1,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
 
@@ -258,14 +269,24 @@ describe('Chart state pointer interactions', () => {
 
     it.skip('should avoid calling projection update listener if moving over the same element with same y', () => {
       MockStore.updateSettings(store, { pointerUpdateTrigger: 'y' });
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 10, y: chartTop + 10 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 10, y: chartTop + 10 },
+          time: 0,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
 
       const tooltipInfo1 = getHighlightedTooltipTooltipValuesSelector(store.getState());
       expect(tooltipInfo1.tooltip.values.length).toBe(1);
       // avoid calls
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 12, y: chartTop + 10 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 12, y: chartTop + 10 },
+          time: 1,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
 
@@ -275,7 +296,12 @@ describe('Chart state pointer interactions', () => {
     });
 
     it.skip('should call projection update listener if moving over the same element with differnt y', () => {
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 10, y: chartTop + 10 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 10, y: chartTop + 10 },
+          time: 0,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
       expect(onPointerUpdateListener.mock.calls[0]?.[0]).toMatchObject({
@@ -289,7 +315,12 @@ describe('Chart state pointer interactions', () => {
       });
 
       // avoid calls
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 10, y: chartTop + 11 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 10, y: chartTop + 11 },
+          time: 1,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(2);
       expect(onPointerUpdateListener.mock.calls[1][0]).toMatchObject({
@@ -304,7 +335,12 @@ describe('Chart state pointer interactions', () => {
     });
 
     it('should call pointer update listeners on move', () => {
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 10, y: chartTop + 10 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 10, y: chartTop + 10 },
+          time: 0,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(1);
       expect(onPointerUpdateListener).toHaveBeenCalledWith({
@@ -324,7 +360,12 @@ describe('Chart state pointer interactions', () => {
       });
 
       // avoid multiple calls for the same value
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 50, y: chartTop + 11 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 50, y: chartTop + 11 },
+          time: 1,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(2);
 
@@ -344,7 +385,12 @@ describe('Chart state pointer interactions', () => {
         smHorizontalValue: null,
       });
 
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 200, y: chartTop + 12 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 200, y: chartTop + 12 },
+          time: 1,
+        }),
+      );
       MockStore.flush(store);
       expect(onPointerUpdateListener).toHaveBeenCalledTimes(3);
       expect(onPointerUpdateListener.mock.calls[2][0]).toEqual({
@@ -501,7 +547,12 @@ describe('Chart state pointer interactions', () => {
       if (scaleType !== ScaleType.Ordinal) {
         scaleOffset = 1;
       }
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 44 + scaleOffset, y: chartTop + 0 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 44 + scaleOffset, y: chartTop + 0 },
+          time: 0,
+        }),
+      );
       let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState());
       expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 0 });
       let cursorBandPosition = getCursorBandPositionSelector(store.getState());
@@ -535,7 +586,12 @@ describe('Chart state pointer interactions', () => {
         ],
       ]);
 
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 45 + scaleOffset, y: chartTop + 0 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 45 + scaleOffset, y: chartTop + 0 },
+          time: 1,
+        }),
+      );
       projectedPointerPosition = getProjectedPointerPositionSelector(store.getState());
       expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 0 });
       cursorBandPosition = getCursorBandPositionSelector(store.getState());
@@ -556,7 +612,12 @@ describe('Chart state pointer interactions', () => {
       if (scaleType !== ScaleType.Ordinal) {
         scaleOffset = 1;
       }
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 44 + scaleOffset, y: chartTop + 89 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 44 + scaleOffset, y: chartTop + 89 },
+          time: 0,
+        }),
+      );
       let projectedPointerPosition = getProjectedPointerPositionSelector(store.getState());
       expect(projectedPointerPosition).toMatchObject({ x: 44 + scaleOffset, y: 89 });
       let cursorBandPosition = getCursorBandPositionSelector(store.getState());
@@ -590,7 +651,12 @@ describe('Chart state pointer interactions', () => {
         ],
       ]);
 
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 45 + scaleOffset, y: chartTop + 89 }, time: 1 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 45 + scaleOffset, y: chartTop + 89 },
+          time: 1,
+        }),
+      );
       projectedPointerPosition = getProjectedPointerPositionSelector(store.getState());
       expect(projectedPointerPosition).toMatchObject({ x: 45 + scaleOffset, y: 89 });
       cursorBandPosition = getCursorBandPositionSelector(store.getState());
@@ -626,7 +692,12 @@ describe('Chart state pointer interactions', () => {
 
       expect(onOutListener).toHaveBeenCalledTimes(0);
 
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 47 + scaleOffset, y: chartTop + 89 }, time: 2 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 47 + scaleOffset, y: chartTop + 89 },
+          time: 2,
+        }),
+      );
     });
 
     test('can hover top-right corner of the chart', () => {
@@ -662,25 +733,45 @@ describe('Chart state pointer interactions', () => {
       }
       let timeCounter = 0;
       for (let i = 0; i < halfWidth; i++) {
-        store.dispatch(onPointerMove({ position: { x: chartLeft + i, y: chartTop + 89 }, time: timeCounter }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + i, y: chartTop + 89 },
+            time: timeCounter,
+          }),
+        );
         expect(onOverListener).toHaveBeenCalledTimes(1);
         expect(onOutListener).toHaveBeenCalledTimes(0);
         timeCounter++;
       }
       for (let i = halfWidth; i < 90; i++) {
-        store.dispatch(onPointerMove({ position: { x: chartLeft + i, y: chartTop + 89 }, time: timeCounter }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + i, y: chartTop + 89 },
+            time: timeCounter,
+          }),
+        );
         expect(onOverListener).toHaveBeenCalledTimes(2);
         expect(onOutListener).toHaveBeenCalledTimes(0);
         timeCounter++;
       }
       for (let i = 0; i < halfWidth; i++) {
-        store.dispatch(onPointerMove({ position: { x: chartLeft + i, y: chartTop + 0 }, time: timeCounter }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + i, y: chartTop + 0 },
+            time: timeCounter,
+          }),
+        );
         expect(onOverListener).toHaveBeenCalledTimes(3);
         expect(onOutListener).toHaveBeenCalledTimes(0);
         timeCounter++;
       }
       for (let i = halfWidth; i < 90; i++) {
-        store.dispatch(onPointerMove({ position: { x: chartLeft + i, y: chartTop + 0 }, time: timeCounter }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + i, y: chartTop + 0 },
+            time: timeCounter,
+          }),
+        );
         expect(onOverListener).toHaveBeenCalledTimes(3);
         expect(onOutListener).toHaveBeenCalledTimes(1);
         timeCounter++;
@@ -688,7 +779,12 @@ describe('Chart state pointer interactions', () => {
     });
 
     test('can hover bottom-right corner of the chart', () => {
-      store.dispatch(onPointerMove({ position: { x: chartLeft + 89, y: chartTop + 89 }, time: 0 }));
+      store.dispatch(
+        onPointerMove({
+          position: { x: chartLeft + 89, y: chartTop + 89 },
+          time: 0,
+        }),
+      );
       const projectedPointerPosition = getProjectedPointerPositionSelector(store.getState());
       // store.setCursorPosition(chartLeft + 99, chartTop + 99);
       expect(projectedPointerPosition).toMatchObject({ x: 89, y: 89 });
@@ -787,7 +883,12 @@ describe('Chart state pointer interactions', () => {
 
       test('chart 0 rotation', () => {
         MockStore.addSpecs([spec, leftAxis, bottomAxis, currentSettingSpec], store);
-        store.dispatch(onPointerMove({ position: { x: chartLeft + 0, y: chartTop + 89 }, time: 0 }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + 0, y: chartTop + 89 },
+            time: 0,
+          }),
+        );
         const tooltipInfo = getHighlightedTooltipTooltipValuesSelector(store.getState());
         expect(tooltipInfo.tooltip.header?.value).toBe(0);
         expect(tooltipInfo.tooltip.header?.formattedValue).toBe('bottom 0');
@@ -802,7 +903,12 @@ describe('Chart state pointer interactions', () => {
         };
         MockStore.addSpecs([spec, leftAxis, bottomAxis, updatedSettings], store);
 
-        store.dispatch(onPointerMove({ position: { x: chartLeft + 0, y: chartTop + 89 }, time: 0 }));
+        store.dispatch(
+          onPointerMove({
+            position: { x: chartLeft + 0, y: chartTop + 89 },
+            time: 0,
+          }),
+        );
         const tooltipInfo = getHighlightedTooltipTooltipValuesSelector(store.getState());
         expect(tooltipInfo.tooltip.header?.value).toBe(1);
         expect(tooltipInfo.tooltip.header?.formattedValue).toBe('left 1');
@@ -812,7 +918,7 @@ describe('Chart state pointer interactions', () => {
     });
     describe('brush', () => {
       test('can respond to a brush end event', () => {
-        const brushEndListener = jest.fn<void, [BrushEvent]>((): void => undefined);
+        const brushEndListener = createMockBrushEndListener();
         const onBrushCaller = createOnBrushEndCaller();
         store.subscribe(() => {
           onBrushCaller(store.getState());
@@ -857,7 +963,7 @@ describe('Chart state pointer interactions', () => {
           expect(brushEndListener).not.toHaveBeenCalled();
         } else {
           expect(brushEndListener).toHaveBeenCalled();
-          expect(brushEndListener).toHaveBeenCalledWith({ x: [0, 2.5] });
+          expect(brushEndListener).toHaveBeenCalledWith({ x: [0, 2.5] }, { keyPressed: noModifierKeysPressed });
         }
         const start2 = { x: 75, y: 0 };
         const end2 = { x: 100, y: 0 };
@@ -874,6 +980,7 @@ describe('Chart state pointer interactions', () => {
 
         const start3 = { x: 75, y: 0 };
         const end3 = { x: 250, y: 0 };
+
         store.dispatch(onMouseDown({ position: start3, time: 700 }));
         store.dispatch(onPointerMove({ position: end3, time: 800 }));
         store.dispatch(onMouseUp({ position: end3, time: 900 }));
@@ -886,6 +993,7 @@ describe('Chart state pointer interactions', () => {
 
         const start4 = { x: 25, y: 0 };
         const end4 = { x: -20, y: 0 };
+
         store.dispatch(onMouseDown({ position: start4, time: 1000 }));
         store.dispatch(onPointerMove({ position: end4, time: 1100 }));
         store.dispatch(onMouseUp({ position: end4, time: 1200 }));
@@ -906,7 +1014,7 @@ describe('Chart state pointer interactions', () => {
         }
       });
       test('can respond to a brush end event on rotated chart', () => {
-        const brushEndListener = jest.fn<void, [BrushEvent]>((): void => undefined);
+        const brushEndListener = createMockBrushEndListener();
         const onBrushCaller = createOnBrushEndCaller();
         store.subscribe(() => {
           onBrushCaller(store.getState());
@@ -938,11 +1046,10 @@ describe('Chart state pointer interactions', () => {
           expect(brushEndListener).not.toHaveBeenCalled();
         } else {
           expect(brushEndListener).toHaveBeenCalled();
-          expect(brushEndListener).toHaveBeenCalledWith({ x: [0, 1] });
+          expect(brushEndListener).toHaveBeenCalledWith({ x: [0, 1] }, { keyPressed: noModifierKeysPressed });
         }
         const start2 = { x: 0, y: 75 };
         const end2 = { x: 0, y: 100 };
-
         store.dispatch(onMouseDown({ position: start2, time: 400 }));
         store.dispatch(onPointerMove({ position: end2, time: 500 }));
         store.dispatch(onMouseUp({ position: end2, time: 600 }));
@@ -978,7 +1085,7 @@ describe('Chart state pointer interactions', () => {
         }
       });
       test('can respond to a Y brush', () => {
-        const brushEndListener = jest.fn<void, [BrushEvent]>((): void => undefined);
+        const brushEndListener = createMockBrushEndListener();
         const onBrushCaller = createOnBrushEndCaller();
         store.subscribe(() => {
           onBrushCaller(store.getState());
@@ -1024,18 +1131,20 @@ describe('Chart state pointer interactions', () => {
           expect(brushEndListener).not.toHaveBeenCalled();
         } else {
           expect(brushEndListener).toHaveBeenCalled();
-          expect(brushEndListener).toHaveBeenCalledWith({
-            y: [
-              {
-                groupId: spec.groupId,
-                extent: [0.75, 3],
-              },
-            ],
-          });
+          expect(brushEndListener).toHaveBeenCalledWith(
+            {
+              y: [
+                {
+                  groupId: spec.groupId,
+                  extent: [0.75, 3],
+                },
+              ],
+            },
+            { keyPressed: noModifierKeysPressed },
+          );
         }
         const start2 = { x: 0, y: 75 };
         const end2 = { x: 0, y: 100 };
-
         store.dispatch(onMouseDown({ position: start2, time: 400 }));
         store.dispatch(onPointerMove({ position: end2, time: 500 }));
         store.dispatch(onMouseUp({ position: end2, time: 600 }));
@@ -1054,7 +1163,7 @@ describe('Chart state pointer interactions', () => {
         }
       });
       test('can respond to rectangular brush', () => {
-        const brushEndListener = jest.fn<void, [BrushEvent]>((): void => undefined);
+        const brushEndListener = createMockBrushEndListener();
         const onBrushCaller = createOnBrushEndCaller();
         store.subscribe(() => {
           onBrushCaller(store.getState());
@@ -1100,19 +1209,21 @@ describe('Chart state pointer interactions', () => {
           expect(brushEndListener).not.toHaveBeenCalled();
         } else {
           expect(brushEndListener).toHaveBeenCalled();
-          expect(brushEndListener).toHaveBeenCalledWith({
-            x: [0, 2.5],
-            y: [
-              {
-                groupId: spec.groupId,
-                extent: [0.75, 3],
-              },
-            ],
-          });
+          expect(brushEndListener).toHaveBeenCalledWith(
+            {
+              x: [0, 2.5],
+              y: [
+                {
+                  groupId: spec.groupId,
+                  extent: [0.75, 3],
+                },
+              ],
+            },
+            { keyPressed: noModifierKeysPressed },
+          );
         }
         const start2 = { x: 75, y: 75 };
         const end2 = { x: 100, y: 100 };
-
         store.dispatch(onMouseDown({ position: start2, time: 400 }));
         store.dispatch(onPointerMove({ position: end2, time: 500 }));
         store.dispatch(onMouseUp({ position: end2, time: 600 }));
diff --git a/packages/charts/src/chart_types/xy_chart/state/chart_state.key_modifiers.test.tsx b/packages/charts/src/chart_types/xy_chart/state/chart_state.key_modifiers.test.tsx
new file mode 100644
index 00000000000..2db585c6c78
--- /dev/null
+++ b/packages/charts/src/chart_types/xy_chart/state/chart_state.key_modifiers.test.tsx
@@ -0,0 +1,114 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Store } from 'redux';
+
+import { createOnBrushEndCaller } from './selectors/on_brush_end_caller';
+import { createOnClickCaller } from './selectors/on_click_caller';
+import { ChartType } from '../..';
+import { simulateBrushSequence, simulateClickSequence } from '../../../mocks/interactions';
+import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs/specs';
+import { createMockBrushEndListener, MockStore } from '../../../mocks/store';
+import { ScaleType } from '../../../scales/constants';
+import { SpecType } from '../../../specs/spec_type';
+import type { GlobalChartState } from '../../../state/chart_state';
+import { getModifierKeys, type KeyPressed, noModifierKeysPressed } from '../../../utils/keys';
+import { SeriesType } from '../utils/specs';
+
+const metaKeyPressed = getModifierKeys({ metaKey: true });
+
+const linearBarSeries = MockSeriesSpec.bar({
+  chartType: ChartType.XYAxis,
+  specType: SpecType.Series,
+  seriesType: SeriesType.Bar,
+  data: [
+    [0, 1],
+    [1, 1],
+    [2, 2],
+    [3, 3],
+  ],
+  xAccessor: 0,
+  yAccessors: [1],
+  xScaleType: ScaleType.Linear,
+  yScaleType: ScaleType.Linear,
+});
+
+const barSeries = MockSeriesSpec.bar({
+  xAccessor: 0,
+  yAccessors: [1],
+  data: [
+    [0, 10],
+    [1, -10],
+    [2, 10],
+  ],
+});
+
+describe('Key Modifiers on BrushEnd', () => {
+  let store: Store<GlobalChartState>;
+  let mockBrushEndListener: ReturnType<typeof createMockBrushEndListener>;
+
+  beforeEach(() => {
+    store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
+    mockBrushEndListener = createMockBrushEndListener();
+    const mockSettingsSpec = MockGlobalSpec.settingsNoMargins({ onBrushEnd: mockBrushEndListener });
+    MockStore.addSpecs([mockSettingsSpec, linearBarSeries], store);
+
+    const onBrushCaller = createOnBrushEndCaller();
+    store.subscribe(() => {
+      onBrushCaller(store.getState());
+    });
+  });
+
+  it('should call the brush end listener with no modifier keys pressed when keyPressed is not explicitly passed', () => {
+    simulateBrushSequence({ store });
+    verifyListenerCall(mockBrushEndListener, noModifierKeysPressed);
+  });
+
+  it('should call the brush end listener with the correct modifier keys', () => {
+    simulateBrushSequence({ store, keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockBrushEndListener, metaKeyPressed);
+
+    simulateBrushSequence({ store, time: [300, 400, 500], keysOnMouseDown: noModifierKeysPressed });
+    verifyListenerCall(mockBrushEndListener, noModifierKeysPressed, 2);
+  });
+});
+
+describe('Key Modifiers on ElementClick', () => {
+  let store: Store<GlobalChartState>;
+  let mockOnElementClick: jest.Mock<void, any[]>;
+
+  beforeEach(() => {
+    store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
+    mockOnElementClick = jest.fn<void, any[]>((): void => undefined);
+    const mockSettingsSpec = MockGlobalSpec.settingsNoMargins({ onElementClick: mockOnElementClick });
+    MockStore.addSpecs([mockSettingsSpec, barSeries], store);
+
+    const onElementClickCaller = createOnClickCaller();
+    store.subscribe(() => {
+      onElementClickCaller(store.getState());
+    });
+  });
+
+  it('should call the element click listener with no modifier keys pressed when keyPressed is not explicitly passed', () => {
+    simulateClickSequence({ store });
+    verifyListenerCall(mockOnElementClick, noModifierKeysPressed);
+  });
+
+  it('should call the element click listener with the correct modifier keys', () => {
+    simulateClickSequence({ store, keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockOnElementClick, metaKeyPressed);
+
+    simulateClickSequence({ store, time: [300, 400, 500], keysOnMouseDown: metaKeyPressed });
+    verifyListenerCall(mockOnElementClick, metaKeyPressed, 2);
+  });
+});
+
+function verifyListenerCall(mockListener: jest.Mock, expectedKeyPressed: KeyPressed, numberOfCalls: number = 1) {
+  expect(mockListener).toHaveBeenCalledTimes(numberOfCalls);
+  expect(mockListener).toHaveBeenNthCalledWith(numberOfCalls, expect.anything(), { keyPressed: expectedKeyPressed });
+}
diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts
index 7bf816313fe..326f1efcfa8 100644
--- a/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts
+++ b/packages/charts/src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts
@@ -25,6 +25,7 @@ import type { GlobalChartState } from '../../../../state/chart_state';
 import { createCustomCachedSelector } from '../../../../state/create_selector';
 import type { DragState } from '../../../../state/pointer_states';
 import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales';
+import { getKeyPressedSelector } from '../../../../state/selectors/get_key_pressed';
 import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
 import type { Rotation } from '../../../../utils/common';
 import { clamp } from '../../../../utils/common';
@@ -62,6 +63,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
           isHistogramModeEnabledSelector,
           computeSmallMultipleScalesSelector,
           getSeriesSpecsSelector,
+          getKeyPressedSelector,
         ],
         (
           lastDrag,
@@ -71,6 +73,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
           histogramMode,
           smallMultipleScales,
           seriesSpec,
+          keyPressed,
         ): void => {
           const nextProps = {
             lastDrag,
@@ -105,7 +108,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
               );
             }
             if (brushAreaEvent.x !== undefined || brushAreaEvent.y !== undefined) {
-              onBrushEnd(brushAreaEvent);
+              onBrushEnd(brushAreaEvent, { keyPressed });
             }
           }
           prevProps = nextProps;
diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts
index 3584c951099..3e12ed78022 100644
--- a/packages/charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts
+++ b/packages/charts/src/chart_types/xy_chart/state/selectors/on_click_caller.ts
@@ -17,10 +17,12 @@ import { AnnotationType } from '../../../../specs';
 import type { GlobalChartState } from '../../../../state/chart_state';
 import { createCustomCachedSelector } from '../../../../state/create_selector';
 import type { PointerState } from '../../../../state/pointer_states';
+import { getKeyPressedSelector } from '../../../../state/selectors/get_key_pressed';
 import { getLastClickSelector } from '../../../../state/selectors/get_last_click';
 import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
 import { isClicking } from '../../../../state/utils/is_clicking';
 import type { IndexedGeometry, GeometryValue } from '../../../../utils/geometry';
+import { type KeyPressed } from '../../../../utils/keys';
 import type { AnnotationTooltipState } from '../../annotations/types';
 import type { XYChartSeriesIdentifier } from '../../utils/series';
 
@@ -48,6 +50,7 @@ export function createOnClickCaller(): (state: GlobalChartState) => void {
         getHighlightedGeomsSelector,
         getProjectedScaledValues,
         getMultipleRectangleAnnotations,
+        getKeyPressedSelector,
       ],
       (
         lastClick: PointerState | null,
@@ -55,11 +58,12 @@ export function createOnClickCaller(): (state: GlobalChartState) => void {
         indexedGeometries: IndexedGeometry[],
         values,
         tooltipStates,
+        keyPressed,
       ): void => {
         if (!isClicking(prevClick, lastClick)) {
           return;
         }
-        const elementClickFired = tryFiringOnElementClick(indexedGeometries, onElementClick);
+        const elementClickFired = tryFiringOnElementClick(indexedGeometries, onElementClick, keyPressed);
         if (!elementClickFired && onAnnotationClick && tooltipStates) {
           tryFiringOnAnnotationClick(tooltipStates, onAnnotationClick, indexedGeometries);
         } else if (!elementClickFired) {
@@ -74,6 +78,7 @@ export function createOnClickCaller(): (state: GlobalChartState) => void {
 function tryFiringOnElementClick(
   indexedGeometries: IndexedGeometry[],
   onElementClick: SettingsSpec['onElementClick'],
+  keyPressed: KeyPressed,
 ): boolean {
   if (indexedGeometries.length === 0 || !onElementClick) {
     return false;
@@ -82,7 +87,7 @@ function tryFiringOnElementClick(
     value,
     seriesIdentifier,
   ]);
-  onElementClick(elements);
+  onElementClick(elements, { keyPressed });
   return true;
 }
 
diff --git a/packages/charts/src/components/chart_container.tsx b/packages/charts/src/components/chart_container.tsx
index 2d3692e62ec..cdb1f573997 100644
--- a/packages/charts/src/components/chart_container.tsx
+++ b/packages/charts/src/components/chart_container.tsx
@@ -87,10 +87,7 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
     }
 
     onPointerMove({
-      position: {
-        x: offsetX,
-        y: offsetY,
-      },
+      position: { x: offsetX, y: offsetY },
       time: timeStamp,
     });
   };
@@ -100,11 +97,14 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
     if (isChartEmpty || disableInteractions || isBrushing) {
       return;
     }
-    onPointerMove({ position: { x: -1, y: -1 }, time: timeStamp });
+    onPointerMove({
+      position: { x: -1, y: -1 },
+      time: timeStamp,
+    });
   };
 
   handleMouseDown = ({
-    nativeEvent: { offsetX, offsetY, timeStamp, button, ctrlKey },
+    nativeEvent: { offsetX, offsetY, timeStamp, button, shiftKey, ctrlKey, altKey, metaKey },
   }: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
     const { isChartEmpty, disableInteractions, onMouseDown, isBrushingAvailable, tooltipState } = this.props;
 
@@ -118,11 +118,9 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
     window.addEventListener('keyup', this.handleKeyUp);
 
     onMouseDown({
-      position: {
-        x: offsetX,
-        y: offsetY,
-      },
+      position: { x: offsetX, y: offsetY },
       time: timeStamp,
+      keyPressed: { shiftKey, ctrlKey, altKey, metaKey },
     });
   };
 
@@ -162,12 +160,8 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
     }
 
     window.removeEventListener('keyup', this.handleKeyUp);
-
     onMouseUp({
-      position: {
-        x: offsetX,
-        y: offsetY,
-      },
+      position: { x: offsetX, y: offsetY },
       time: timeStamp,
     });
   };
@@ -189,7 +183,10 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
     const { onMouseUp } = this.props;
 
     window.removeEventListener('mouseup', this.handleBrushEnd);
-    onMouseUp({ position: { x: -1, y: -1 }, time: Date.now() });
+    onMouseUp({
+      position: { x: -1, y: -1 },
+      time: Date.now(),
+    });
   };
 
   render() {
diff --git a/packages/charts/src/mocks/interactions/index.ts b/packages/charts/src/mocks/interactions/index.ts
new file mode 100644
index 00000000000..4cab05ba2f4
--- /dev/null
+++ b/packages/charts/src/mocks/interactions/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+export { simulateBrushSequence, simulateClickSequence } from './interactions';
diff --git a/packages/charts/src/mocks/interactions/interactions.ts b/packages/charts/src/mocks/interactions/interactions.ts
new file mode 100644
index 00000000000..810eae47df3
--- /dev/null
+++ b/packages/charts/src/mocks/interactions/interactions.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { Store } from 'redux';
+
+import type { KeyPressed } from '../../specs';
+import { onMouseDown, onMouseUp, onPointerMove } from '../../state/actions/mouse';
+import type { GlobalChartState } from '../../state/chart_state';
+
+const INITIAL_POSITION = { x: 0, y: 0 };
+const POSITION = { x: 50, y: 75 };
+
+export function simulateBrushSequence({
+  store,
+  start = INITIAL_POSITION,
+  end = POSITION,
+  time = [0, 100, 200],
+  keysOnMouseDown = undefined,
+}: {
+  store: Store<GlobalChartState>;
+  start?: { x: number; y: number };
+  end?: { x: number; y: number };
+  time?: [number, number, number];
+  keysOnMouseDown?: KeyPressed;
+}) {
+  store.dispatch(onMouseDown({ position: start, time: time[0], keyPressed: keysOnMouseDown }));
+  store.dispatch(onPointerMove({ position: end, time: time[1] }));
+  store.dispatch(onMouseUp({ position: end, time: time[2] }));
+}
+
+export function simulateClickSequence({
+  store,
+  position = POSITION,
+  time = [0, 100, 200],
+  keysOnMouseDown = undefined,
+}: {
+  store: Store<GlobalChartState>;
+  position?: { x: number; y: number };
+  time?: [number, number, number];
+  keysOnMouseDown?: KeyPressed;
+}) {
+  store.dispatch(onPointerMove({ position, time: time[0] }));
+  store.dispatch(onMouseDown({ position, time: time[1], keyPressed: keysOnMouseDown }));
+  store.dispatch(onMouseUp({ position, time: time[2] }));
+}
diff --git a/packages/charts/src/mocks/store/brush_end_listener.ts b/packages/charts/src/mocks/store/brush_end_listener.ts
new file mode 100644
index 00000000000..7b0569cb6c6
--- /dev/null
+++ b/packages/charts/src/mocks/store/brush_end_listener.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { BrushEndListener } from '../../specs';
+
+export function createMockBrushEndListener() {
+  return jest.fn<ReturnType<BrushEndListener>, Parameters<BrushEndListener>>((): void => undefined);
+}
diff --git a/packages/charts/src/mocks/store/index.ts b/packages/charts/src/mocks/store/index.ts
index b8acc6596c7..bb697e1e9ad 100644
--- a/packages/charts/src/mocks/store/index.ts
+++ b/packages/charts/src/mocks/store/index.ts
@@ -8,3 +8,4 @@
 
 /** @internal */
 export * from './store';
+export * from './brush_end_listener';
diff --git a/packages/charts/src/specs/settings.tsx b/packages/charts/src/specs/settings.tsx
index c6cb54b76e7..1ddf4d79eca 100644
--- a/packages/charts/src/specs/settings.tsx
+++ b/packages/charts/src/specs/settings.tsx
@@ -44,6 +44,7 @@ import { stripUndefined } from '../utils/common';
 import type { Dimensions } from '../utils/dimensions';
 import type { GeometryValue } from '../utils/geometry';
 import type { GroupId, SpecId } from '../utils/ids';
+import type { KeyPressed } from '../utils/keys';
 import type { SeriesCompareFn } from '../utils/series_sort';
 import type { PartialTheme, PointStyle, Theme } from '../utils/themes/theme';
 
@@ -88,27 +89,59 @@ export interface FlameLayerValue {
   vmIndex: number;
 }
 
-/** @public */
+/**
+ * Represents the extent of a brush interaction for a specific group in an XY chart.
+ *
+ * @public
+ */
 export interface GroupBrushExtent {
   groupId: GroupId;
   extent: [number, number];
 }
 
-/** @public */
+/**
+ * Represents the details of a brush interaction in an XY chart.
+ *
+ * @public
+ */
 export interface XYBrushEvent {
   x?: [number, number];
   y?: Array<GroupBrushExtent>;
 }
 
-/** @public */
+/**
+ * Represents an interaction event with an element in an XY chart.
+ *
+ * @public
+ */
 export type XYChartElementEvent = [geometry: GeometryValue, seriesIdentifier: XYChartSeriesIdentifier];
-/** @public */
+
+/**
+ * Represents an interaction event with an element in a partition chart.
+ *
+ * @public
+ */
 export type PartitionElementEvent = [layers: Array<LayerValue>, seriesIdentifier: SeriesIdentifier];
-/** @public */
+
+/**
+ * Represents an interaction event with an element in a flame chart.
+ *
+ * @public
+ */
 export type FlameElementEvent = FlameLayerValue;
-/** @public */
+
+/**
+ * Represents an interaction event with a cell in a heatmap chart.
+ *
+ * @public
+ */
 export type HeatmapElementEvent = [cell: Cell, seriesIdentifier: SeriesIdentifier];
-/** @public */
+
+/**
+ * Represents an interaction event with an element in a word cloud chart.
+ *
+ * @public
+ */
 export type WordCloudElementEvent = [model: WordModel, seriesIdentifier: SeriesIdentifier];
 
 /**
@@ -138,7 +171,17 @@ export function isMetricElementEvent(e: Parameters<ElementClickListener>[0][0]):
  */
 export type ProjectionClickListener = (values: ProjectedValues) => void;
 
-/** @public */
+/**
+ * The listener type for the `onElementClick` event.
+ *
+ * This listener is triggered when an element in the chart is clicked, providing details about the clicked elements
+ * and any modifier keys that were pressed during the interaction.
+ *
+ * @param elements - An array of clicked elements.
+ * @param options - Additional options, including the state of modifier keys (`keyPressed`).
+ *
+ * @public
+ */
 export type ElementClickListener = (
   elements: Array<
     | XYChartElementEvent
@@ -148,6 +191,7 @@ export type ElementClickListener = (
     | WordCloudElementEvent
     | MetricElementEvent
   >,
+  options?: { keyPressed: KeyPressed },
 ) => void;
 
 /** @public */
@@ -162,16 +206,42 @@ export type ElementOverListener = (
   >,
 ) => void;
 
-/** @public */
+/**
+ * Represents the details of a brush interaction event.
+ *
+ * A `BrushEvent` can be one of the following:
+ * - `XYBrushEvent`: Represents a brush interaction in an XY chart.
+ * - `HeatmapBrushEvent`: Represents a brush interaction in a Heatmap chart.
+ *
+ * This type is used in listeners like `onBrushEnd` to provide information about the brushed area.
+ *
+ * @public
+ */
 export type BrushEvent = XYBrushEvent | HeatmapBrushEvent;
 
-/** @public */
-export type BrushEndListener = (brushAreaEvent: BrushEvent) => void;
+/**
+ * The listener type for the `onBrushEnd` event.
+ *
+ * This listener is triggered when a brush interaction ends, providing details about the brushed area
+ * and any modifier keys that were pressed during the interaction.
+ *
+ * @param brushAreaEvent - The details of the brushed area, which can be an `XYBrushEvent` or `HeatmapBrushEvent`.
+ * @param options - Additional options, including the state of modifier keys (`keyPressed`).
+ *
+ * @public
+ */
+export type BrushEndListener = (brushAreaEvent: BrushEvent, options?: { keyPressed: KeyPressed }) => void;
 
 /** @public */
 export type ProjectionAreaChangeListener = (areas: { projection: Dimensions; parent: Dimensions }) => void;
 
-/** @public */
+/**
+ * Represents the details of a brush interaction in a Heatmap chart.
+ *
+ * This interface provides information about the brushed cells and the corresponding X and Y values.
+ *
+ * @public
+ */
 export interface HeatmapBrushEvent extends SmallMultiplesDatum {
   cells: Cell[];
   x: (string | number)[];
@@ -649,3 +719,5 @@ export function isPointerOutEvent(event: PointerEvent | null | undefined): event
 export function isPointerOverEvent(event: PointerEvent | null | undefined): event is PointerOverEvent {
   return event?.type === PointerEventType.Over;
 }
+
+export type { KeyPressed, ModifierKeys } from '../utils/keys';
diff --git a/packages/charts/src/state/actions/mouse.ts b/packages/charts/src/state/actions/mouse.ts
index 28fce3f396b..c0e42545f03 100644
--- a/packages/charts/src/state/actions/mouse.ts
+++ b/packages/charts/src/state/actions/mouse.ts
@@ -8,11 +8,13 @@
 
 import { createAction } from '@reduxjs/toolkit';
 
+import type { KeyPressed } from '../../utils/keys';
 import type { Point } from '../../utils/point';
 
 interface MouseAction {
   position: Point;
   time: number;
+  keyPressed?: KeyPressed;
 }
 
 /**
diff --git a/packages/charts/src/state/pointer_states.ts b/packages/charts/src/state/pointer_states.ts
index 0e59466d285..d002d87c324 100644
--- a/packages/charts/src/state/pointer_states.ts
+++ b/packages/charts/src/state/pointer_states.ts
@@ -6,6 +6,7 @@
  * Side Public License, v 1.
  */
 
+import type { KeyPressed } from '../utils/keys';
 import type { Point } from '../utils/point';
 
 /** @internal */
@@ -29,4 +30,5 @@ export interface PointerStates {
   up: PointerState | null;
   lastDrag: DragState | null;
   lastClick: PointerState | null;
+  keyPressed: KeyPressed;
 }
diff --git a/packages/charts/src/state/reducers/interactions.ts b/packages/charts/src/state/reducers/interactions.ts
index 6420de8740a..9d03e2ce3c5 100644
--- a/packages/charts/src/state/reducers/interactions.ts
+++ b/packages/charts/src/state/reducers/interactions.ts
@@ -16,6 +16,7 @@ import { getPickedShapesLayerValues } from '../../chart_types/partition_chart/st
 import type { LegendItem } from '../../common/legend';
 import type { SeriesIdentifier } from '../../common/series_id';
 import type { TooltipValue } from '../../specs/tooltip';
+import { noModifierKeysPressed } from '../../utils/keys';
 import { getDelta } from '../../utils/point';
 import { onDOMElementEnter, onDOMElementLeave } from '../actions/dom_element';
 import { onKeyPress } from '../actions/key';
@@ -77,6 +78,7 @@ export const handleMouseActions = (builder: ActionReducerMapBuilder<ChartSliceSt
       state.pointer.dragging = dragging;
       state.pointer.current.position = action.payload.position;
       state.pointer.current.time = action.payload.time;
+      // Do not update state.pointer.keyPressed here, only the mouseDown event should store the pressed keys
     })
     .addCase(onMouseDown, (globalState, action) => {
       if (getInternalIsInitializedSelector(globalState) !== InitStatus.Initialized) return;
@@ -90,6 +92,7 @@ export const handleMouseActions = (builder: ActionReducerMapBuilder<ChartSliceSt
         position: action.payload.position,
         time: action.payload.time,
       };
+      state.pointer.keyPressed = action.payload.keyPressed ?? noModifierKeysPressed;
     })
     .addCase(onMouseUp, (globalState, action) => {
       if (getInternalIsInitializedSelector(globalState) !== InitStatus.Initialized) return;
@@ -129,6 +132,7 @@ export const handleMouseActions = (builder: ActionReducerMapBuilder<ChartSliceSt
         position: action.payload.position,
         time: action.payload.time,
       };
+      // Do not update state.pointer.keyPressed here, only the mouseDown event should store the pressed keys
     });
 };
 
diff --git a/packages/charts/src/state/selectors/get_key_pressed.ts b/packages/charts/src/state/selectors/get_key_pressed.ts
new file mode 100644
index 00000000000..d4bba819753
--- /dev/null
+++ b/packages/charts/src/state/selectors/get_key_pressed.ts
@@ -0,0 +1,15 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { noModifierKeysPressed } from '../../utils/keys';
+import type { GlobalChartState } from '../chart_state';
+
+/** @internal */
+export function getKeyPressedSelector(state: GlobalChartState) {
+  return state.interactions.pointer.keyPressed ?? noModifierKeysPressed;
+}
diff --git a/packages/charts/src/state/utils.ts b/packages/charts/src/state/utils.ts
index 116ab0d91aa..286e128fe3e 100644
--- a/packages/charts/src/state/utils.ts
+++ b/packages/charts/src/state/utils.ts
@@ -11,6 +11,7 @@ import type { PointerState, PointerStates } from './pointer_states';
 import type { ChartType } from '../chart_types';
 import type { Spec } from '../specs/spec';
 import type { SpecList } from '../specs/spec_type';
+import { noModifierKeysPressed } from '../utils/keys';
 
 /**
  * Returns all matching specs
@@ -52,6 +53,7 @@ export const getInitialPointerState = (): PointerStates => ({
   up: null,
   lastDrag: null,
   lastClick: null,
+  keyPressed: noModifierKeysPressed,
 });
 
 /** @internal */
diff --git a/packages/charts/src/state/utils/get_initial_pointer_state.ts b/packages/charts/src/state/utils/get_initial_pointer_state.ts
index 77a6e617a01..8d48d466010 100644
--- a/packages/charts/src/state/utils/get_initial_pointer_state.ts
+++ b/packages/charts/src/state/utils/get_initial_pointer_state.ts
@@ -6,6 +6,7 @@
  * Side Public License, v 1.
  */
 
+import { noModifierKeysPressed } from '../../utils/keys';
 import type { PointerStates } from '../pointer_states';
 
 /** @internal */
@@ -17,4 +18,5 @@ export const getInitialPointerState = (): PointerStates => ({
   up: null,
   lastDrag: null,
   lastClick: null,
+  keyPressed: noModifierKeysPressed,
 });
diff --git a/packages/charts/src/utils/keys.ts b/packages/charts/src/utils/keys.ts
new file mode 100644
index 00000000000..60493608d9c
--- /dev/null
+++ b/packages/charts/src/utils/keys.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+/**
+ * Represents the available modifier keys (e.g., Shift, Control, Alt, Meta).
+ * These keys correspond to the `MouseEvent` instance properties `shiftKey`, `ctrlKey`, `altKey` and `metaKey`.
+ * For more details, see: https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent#instance_properties
+ *
+ * @public
+ */
+export type ModifierKeys = 'shiftKey' | 'ctrlKey' | 'altKey' | 'metaKey';
+
+/**
+ * Represents the state of modifier keys during an interaction.
+ * Each key is a boolean indicating whether it is pressed (`true`) or not (`false`).
+ *
+ * @public
+ */
+export type KeyPressed = Record<ModifierKeys, boolean>;
+
+/** @internal */
+export const noModifierKeysPressed: KeyPressed = { shiftKey: false, ctrlKey: false, altKey: false, metaKey: false };
+
+/** @internal */
+export function getModifierKeys(overrides: Partial<KeyPressed> = {}): KeyPressed {
+  return { ...noModifierKeysPressed, ...overrides };
+}
diff --git a/packages/charts/src/utils/point.ts b/packages/charts/src/utils/point.ts
index c094f7e96ea..64ee11b43e8 100644
--- a/packages/charts/src/utils/point.ts
+++ b/packages/charts/src/utils/point.ts
@@ -12,7 +12,7 @@ export interface Point {
   y: number;
 }
 
-/** @internal * */
+/** @internal */
 export function getDelta(start: Point, end: Point) {
   return Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2));
 }


Reviews:
user: login: mariairiartef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T09:39:00Z
user: login: mariairiartef
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T09:42:15Z
user: login: markov00
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-06T16:54:12Z
user: login: markov00
body: Looks great to me, thanks!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-07T10:49:09Z

Review comments:
diff_hunk: @@ -17,4 +17,5 @@ export const getInitialPointerState = (): PointerStates => ({
   up: null,
   lastDrag: null,
   lastClick: null,
+  keyPressed: null,
path: packages/charts/src/state/utils/get_initial_pointer_state.ts
user: login: mariairiartef
body: add  no modifier key press and not being null
created_at: 2025-04-28T09:39:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1131,6 +1293,125 @@ describe('Chart state pointer interactions', () => {
           });
         }
       });
+      test('can handle modifier keys during brush events', () => {
path: packages/charts/src/chart_types/xy_chart/state/chart_state.interactions.test.tsx
user: login: mariairiartef
body: Add this test case in a separate test file for checking modifier keys
created_at: 2025-04-28T09:42:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,9 +60,9 @@ describe('Categorical heatmap brush', () => {
   it('should brush on categorical scale', () => {
     const caller = createOnBrushEndCaller();
     store.dispatch(onPointerMove({ position: { x: 50, y: 50 }, time: 0 }));
-    store.dispatch(onMouseDown({ position: { x: 50, y: 50 }, time: 100 }));
+    store.dispatch(onMouseDown({ position: { x: 50, y: 50 }, time: 100, keyPressed: noModifierKeysPressed }));
     store.dispatch(onPointerMove({ position: { x: 150, y: 250 }, time: 200 }));
-    store.dispatch(onMouseUp({ position: { x: 150, y: 250 }, time: 300 }));
+    store.dispatch(onMouseUp({ position: { x: 150, y: 250 }, time: 300, keyPressed: noModifierKeysPressed }));
path: packages/charts/src/chart_types/heatmap/state/selectors/get_brushed_highlighted_shapes.test.ts
user: login: markov00
body: I believe we can make the `keyPressed` optional in the the `onMouseDown` action.
Within the reducer, if missing, we add the default `noModifierKeyPRessed`.

For the `onMouseUp` action instead, we can completely remove the `keyPressed` because it is not used in the reducer at all.

created_at: 2025-05-06T16:45:16Z
author_association: COLLABORATOR
diff_hunk: @@ -23,6 +23,7 @@ import { onPointerMove } from '../../../state/actions/mouse';
 import { upsertSpec, specParsed } from '../../../state/actions/specs';
 import { createChartStore, type GlobalChartState } from '../../../state/chart_state';
 import { chartSelectorsRegistry } from '../../../state/selectors/get_internal_chart_state';
+import { noModifierKeysPressed } from '../../../utils/keys';
path: packages/charts/src/chart_types/xy_chart/state/chart_state.timescales.test.ts
user: login: markov00
body: I don't think this is used in this file, can be removed
created_at: 2025-05-06T16:48:13Z
author_association: COLLABORATOR
diff_hunk: @@ -16,6 +16,7 @@ import { onPointerMove } from '../../../state/actions/mouse';
 import { upsertSpec, specParsed } from '../../../state/actions/specs';
 import { createChartStore, type GlobalChartState } from '../../../state/chart_state';
 import { chartSelectorsRegistry } from '../../../state/selectors/get_internal_chart_state';
+import { noModifierKeysPressed } from '../../../utils/keys';
path: packages/charts/src/chart_types/xy_chart/state/chart_state.tooltip.test.ts
user: login: markov00
body: not used I believe
created_at: 2025-05-06T16:48:23Z
author_association: COLLABORATOR
diff_hunk: @@ -14,6 +14,7 @@ import { MockStore } from '../../../../mocks/store/store';
 import { ScaleType } from '../../../../scales/constants';
 import { onPointerMove } from '../../../../state/actions/mouse';
 import type { GlobalChartState } from '../../../../state/chart_state';
+import { noModifierKeysPressed } from '../../../../utils/keys';
path: packages/charts/src/chart_types/xy_chart/state/selectors/get_elements_at_cursor_pos.test.ts
user: login: markov00
body: not used
created_at: 2025-05-06T16:48:34Z
author_association: COLLABORATOR
diff_hunk: @@ -14,6 +14,7 @@ import { MockStore } from '../../../../mocks/store/store';
 import { ScaleType } from '../../../../scales/constants';
 import { onPointerMove } from '../../../../state/actions/mouse';
 import type { GlobalChartState } from '../../../../state/chart_state';
+import { noModifierKeysPressed } from '../../../../utils/keys';
path: packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.test.ts
user: login: markov00
body: not used
created_at: 2025-05-06T16:48:41Z
author_association: COLLABORATOR
diff_hunk: @@ -45,4 +47,4 @@ export const onMouseUp = createAction<MouseAction>('ON_MOUSE_UP');
  * @param time the timestamp of the event (native event timeStamp)
  * @internal
  */
-export const onPointerMove = createAction<MouseAction>('ON_POINTER_MOVE');
+export const onPointerMove = createAction<Omit<MouseAction, 'keyPressed'>>('ON_POINTER_MOVE');
path: packages/charts/src/state/actions/mouse.ts
user: login: markov00
body: Given the comments above, you can probably keep the original `MouseAction` interface and adds the `keyPressed` property only to the onMouseDown
created_at: 2025-05-06T16:51:56Z
author_association: COLLABORATOR
diff_hunk: @@ -129,6 +131,7 @@ export const handleMouseActions = (builder: ActionReducerMapBuilder<ChartSliceSt
         position: action.payload.position,
         time: action.payload.time,
       };
+      // Do not update state.pointer.keyPressed here
path: packages/charts/src/state/reducers/interactions.ts
user: login: markov00
body: ```suggestion
      // Do not update state.pointer.keyPressed here, only the mouseDown event should store the pressed keys
```
created_at: 2025-05-06T16:53:16Z
author_association: COLLABORATOR

PR comments: