Repository: elastic/kibana
Pull request #218233: [ES|QL] [Lens] Display a keyword with the correct format in a metric visualization
Author: login: andrimal
Labels: name: release_note:fix
name: Team:Visualizations
description: Team label for Lens, elastic-charts, Graph, legacy editors (TSVB, Visualize, Timelion) t//
name: Feature:Lens
name: backport:version
description: Backport to applied version labels
name: v9.1.0
name: v8.19.0
name: v8.18.1
name: v9.0.1
Created: 2025-04-15T10:36:48Z | Merged: 2025-04-23T09:47:56Z

## Summary

Fix #203526 

- Example 1: Before the fix, the value was formatted as a number (screenshot 1). After the fix, the value remains a string (screenshot 2).

<img width="1506" alt="Screenshot 2025-04-15 at 1 26 26 PM" src="https://github.com/user-attachments/assets/74da3971-b4df-4602-a477-5f83ddade521" />
<img width="1495" alt="Screenshot 2025-04-15 at 1 29 37 PM" src="https://github.com/user-attachments/assets/6c0cb412-95c0-47de-9765-4c3eae4e2fa8" />

- Example 2: Before the fix (screenshot 3) and after the fix (screenshot 4)
<img width="862" alt="Screenshot 2025-04-16 at 2 18 23 PM" src="https://github.com/user-attachments/assets/9eafe58f-edd7-43d6-97d1-2638f2f3e906" />
<img width="849" alt="Screenshot 2025-04-16 at 2 17 26 PM" src="https://github.com/user-attachments/assets/5a912305-22c2-43b2-a3b8-a412310785a7" />


### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios




<!--ONMERGE {"backportTargets":["8.18","8.19","9.0"]} ONMERGE-->

Linked issue #203526: [ES|QL] [Lens] Display a keyword in a metric viz correctly
**Describe the feature:**

We want to visualize a keyword field in the metric viz but this is rendered as a number wrongly. As an example this:

```
FROM logs-*
| limit 1
| eval now=now()
| eval day_number=DATE_EXTRACT("DAY_OF_WEEK",now)
| eval closing_time=concat(DATE_FORMAT("yyyy-MM-dd", now),case(day_number >= 1 AND day_number <= 4, "23:15", day_number == 5, "02:15"))| eval closing_time = DATE_PARSE("yyyy-MM-ddHH:mm", closing_time)
| eval diff = case(day_number >= 1 AND day_number <= 4, DATE_DIFF("second", now, closing_time), day_number == 5, date_diff("second", now, closing_time+1day))
// diff is now in seconds.
// we still do not have a formatter in ES|QL
// it should be coming soon :tm:
| eval hour = diff / 3600.0 // this creates us the hour handle
| eval minutes = concat("0.",mv_slice(split(hour::string, "."),1))::double * 60 // this creates the minute handle
| eval seconds = concat("0.",mv_slice(split(minutes::string, "."),1))::double * 60 // this creates the minute handle
| eval `Closing in` = CONCAT(mv_slice(split(hour::string, "."),0), "h",mv_slice(split(minutes::string, "."),0), "m",mv_slice(split(seconds::string, "."),0), "s")
| keep `Closing in`
```

will return:

![Image](https://github.com/user-attachments/assets/8759c21d-072f-41b7-8a17-15df69ad51b7)

but when I am selecting the metric viz the value changes to 13.

![Image](https://github.com/user-attachments/assets/14d53174-d5f9-47ca-b82d-ce697786f8fe)

It would be cool if the value could keep its type and not be parsed as a number
Linked issue comments: user: login: elasticmachine
created_at: 2024-12-10T10:07:48Z
body: Pinging @elastic/kibana-visualizations (Team:Visualizations)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2024-12-10T10:07:49Z
body: Pinging @elastic/kibana-esql (Team:ESQL)
author_association: CONTRIBUTOR

Diff:
diff --git a/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.test.tsx b/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
index de6ca83622050..ffffc80a9d87b 100644
--- a/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
+++ b/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
@@ -1508,4 +1508,51 @@ describe('MetricVisComponent', function () {
       expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
     });
   });
+
+  describe('ES|QL metric chart', () => {
+    it('keyword value should remain string', () => {
+      const dataFromESQL: Datatable = {
+        type: 'datatable',
+        columns: [
+          {
+            id: 'a',
+            name: 'alpha',
+            meta: {
+              esType: 'keyword',
+              type: 'string',
+              sourceParams: {
+                indexPattern: 'index',
+              },
+            },
+          },
+        ],
+        rows: [{ a: '12h50m30s' }],
+        meta: {
+          type: 'es_ql',
+        },
+      };
+
+      const config: Props['config'] = {
+        dimensions: {
+          metric: 'a',
+        },
+        metric: {
+          color: '#FFFFFF',
+          iconAlign: 'left',
+          maxCols: 3,
+          titlesTextAlign: 'left',
+          valueFontSize: 'default',
+          valuesTextAlign: 'right',
+        },
+      };
+
+      const component = shallow(
+        <MetricVis config={config} data={dataFromESQL} {...defaultProps} />
+      );
+
+      const { data } = component.find(Metric).props();
+      const formattedMetricValue = data[0][0]!.value;
+      expect(formattedMetricValue).toEqual('string-12h50m30s');
+    });
+  });
 });
diff --git a/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.tsx b/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.tsx
index 00fe4ac948f05..538a704b65caa 100644
--- a/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.tsx
+++ b/src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.tsx
@@ -93,7 +93,9 @@ const getMetricFormatter = (
   accessor: ExpressionValueVisDimension | string,
   columns: Datatable['columns']
 ) => {
-  const serializedFieldFormat = getFormatByAccessor(accessor, columns);
+  const type = getColumnByAccessor(accessor, columns)?.meta.type;
+  const defaultFormat = type ? { id: type } : undefined;
+  const serializedFieldFormat = getFormatByAccessor(accessor, columns, defaultFormat);
   const enhancedFieldFormat = enhanceFieldFormat(serializedFieldFormat);
   return getFormatService().deserialize(enhancedFieldFormat).getConverterFor('text');
 };
@@ -133,7 +135,7 @@ const buildFilterEvent = (rowIdx: number, columnIdx: number, table: Datatable) =
 const getIcon =
   (type: string) =>
   ({ width, height, color }: { width: number; height: number; color: string }) =>
-    <EuiIcon type={type} fill={color} style={{ width, height }} />;
+    <EuiIcon type={type} fill={color} css={{ width, height }} />;
 
 export interface MetricVisComponentProps {
   data: Datatable;


Reviews:
user: login: andrimal
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T10:44:40Z
user: login: dej611
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:42:12Z
user: login: dej611
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T08:39:47Z

Review comments:
diff_hunk: @@ -133,7 +135,7 @@ const buildFilterEvent = (rowIdx: number, columnIdx: number, table: Datatable) =
 const getIcon =
   (type: string) =>
   ({ width, height, color }: { width: number; height: number; color: string }) =>
-    <EuiIcon type={type} fill={color} style={{ width, height }} />;
+    <EuiIcon type={type} fill={color} css={{ width, height }} />;
path: src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.tsx
user: login: andrimal
body: This is an automatic eslint fix.
created_at: 2025-04-15T10:44:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1508,4 +1508,51 @@ describe('MetricVisComponent', function () {
       expect(settingsComponent.prop('ariaUseDefaultSummary')).toEqual(true);
     });
   });
+
+  describe('ES|QL metric chart', () => {
+    const dataFromESQL: Datatable = {
+      type: 'datatable',
+      columns: [
+        {
+          id: 'a',
+          name: 'alpha',
+          meta: {
+            esType: 'keyword',
+            type: 'string',
+            sourceParams: {
+              indexPattern: 'index',
+            },
+          },
+        },
+      ],
+      rows: [{ a: '12h50m30s' }],
+      meta: {
+        type: 'es_ql',
+      },
+    };
+
+    const config: Props['config'] = {
+      dimensions: {
+        metric: 'a',
+      },
+      metric: {
+        color: '#FFFFFF',
+        iconAlign: 'left',
+        maxCols: 3,
+        titlesTextAlign: 'left',
+        valueFontSize: 'default',
+        valuesTextAlign: 'right',
+      },
+    };
path: src/platform/plugins/shared/chart_expressions/expression_metric/public/components/metric_vis.test.tsx
user: login: dej611
body: nit: this looks like a test specific configuration rather than a generic thing for the ES|QL scenario. Can you move them into the test itself rather than here?
created_at: 2025-04-17T16:42:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: elasticmachine
created_at: 2025-04-16T16:01:19Z
body: Pinging @elastic/kibana-visualizations (Team:Visualizations)
author_association: CONTRIBUTOR
user: login: elasticmachine
created_at: 2025-04-22T09:49:37Z
body: ## :yellow_heart: Build succeeded, but was flaky
* [Buildkite Build](https://buildkite.com/elastic/kibana-pull-request/builds/294741)
* Commit: 84c64ae2586a5082545167a2b4689c343be8be74

### Failed CI Steps
* [Jest Integration Tests #2](https://buildkite.com/elastic/kibana-pull-request/builds/294741#01965c4d-8dd8-42be-91a8-b733abbad3f6)

### Metrics [[docs](https://www.elastic.co/guide/en/kibana/master/ci-metrics.html)]
#### Async chunks
> Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app


| id | [before](https://github.com/elastic/kibana/commit/09eb9e0c39b76e5384de73253793bc5528342250) | [after](https://github.com/elastic/kibana/commit/84c64ae2586a5082545167a2b4689c343be8be74) | diff |
| --- | --- | --- | --- |
| `expressionMetricVis` | 5.1KB | 5.2KB | +116.0B |



### History
* :green_heart: [Build #293705](https://buildkite.com/elastic/kibana-pull-request/builds/293705) succeeded 6c2ed7d36dc5b952b2f392070a6184c5a3aabdfb
* :broken_heart: [Build #293292](https://buildkite.com/elastic/kibana-pull-request/builds/293292) failed eae89d564a483c79461648e0cd4f93f48e18105a
* :broken_heart: [Build #293129](https://buildkite.com/elastic/kibana-pull-request/builds/293129) failed 33d2c052c15e14cbcb1feba9d53b91834caf6286

<!--buildkite-pr-comment-kibana-pull-request
{"builds":[{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/294741","number":294741,"commit":"84c64ae2586a5082545167a2b4689c343be8be74"},{"buildStatus":{"state":"passed","success":true,"hasRetries":true,"hasNonPreemptionRetries":false},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293705","number":293705,"commit":"6c2ed7d36dc5b952b2f392070a6184c5a3aabdfb"},{"buildStatus":{"state":"failing","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293292","number":293292,"commit":"eae89d564a483c79461648e0cd4f93f48e18105a"},{"buildStatus":{"state":"failed","success":false,"hasRetries":true,"hasNonPreemptionRetries":true},"url":"https://buildkite.com/elastic/kibana-pull-request/builds/293129","number":293129,"commit":"33d2c052c15e14cbcb1feba9d53b91834caf6286"}],"number":294741}
buildkite-pr-comment-->
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-23T09:48:14Z
body: Starting backport for target branches: 8.18, 8.19, 9.0

https://github.com/elastic/kibana/actions/runs/14615132102
author_association: CONTRIBUTOR
user: login: kibanamachine
created_at: 2025-04-23T09:54:52Z
body: ## 💚 All backports created successfully

| Status | Branch | Result |
|:------:|:------:|:------|
|✅|8.18|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/218932">](https://github.com/elastic/kibana/pull/218932)|
|✅|8.19|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/218933">](https://github.com/elastic/kibana/pull/218933)|
|✅|9.0|[<img src="https://img.shields.io/github/pulls/detail/state/elastic/kibana/218934">](https://github.com/elastic/kibana/pull/218934)|

Note: Successful backport PRs will be merged automatically after passing CI.

### Questions ?
Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport)

<!--- Backport version: 9.6.6 -->
author_association: CONTRIBUTOR