{
+ const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem;
+ return datum.latency ?? 0;
+ })
+ );
+ return getDurationFormatter(maxLatency);
+}
+
+export default {
+ title: 'shared/charts/InstancesLatencyDistributionChart/CustomTooltip',
+ component: CustomTooltip,
+ decorators: [
+ (Story: ComponentType) => (
+
+
+
+ ),
+ ],
+};
+
+export function Example(props: TooltipInfo) {
+ return (
+
+ );
+}
+Example.args = {
+ header: {
+ seriesIdentifier: {
+ key:
+ 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}',
+ specId: 'Instances',
+ yAccessor: '(index:0)',
+ splitAccessors: {},
+ seriesKeys: ['(index:0)'],
+ },
+ valueAccessor: 'y1',
+ label: 'Instances',
+ value: 9.473837632998105,
+ formattedValue: '9.473837632998105',
+ markValue: null,
+ color: '#6092c0',
+ isHighlighted: false,
+ isVisible: true,
+ datum: {
+ serviceNodeName:
+ '2f3221afa3f00d3bc07069d69efd5bd4c1607be6155a204551c8fe2e2b5dd750',
+ errorRate: 0.03496503496503497,
+ latency: 1057231.4125874126,
+ throughput: 9.473837632998105,
+ cpuUsage: 0.000033333333333333335,
+ memoryUsage: 0.18701022939403547,
+ },
+ },
+ values: [
+ {
+ seriesIdentifier: {
+ key:
+ 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}',
+ specId: 'Instances',
+ },
+ valueAccessor: 'y1',
+ label: 'Instances',
+ value: 1057231.4125874126,
+ formattedValue: '1057231.4125874126',
+ markValue: null,
+ color: '#6092c0',
+ isHighlighted: true,
+ isVisible: true,
+ datum: {
+ serviceNodeName:
+ '2f3221afa3f00d3bc07069d69efd5bd4c1607be6155a204551c8fe2e2b5dd750',
+ errorRate: 0.03496503496503497,
+ latency: 1057231.4125874126,
+ throughput: 9.473837632998105,
+ cpuUsage: 0.000033333333333333335,
+ memoryUsage: 0.18701022939403547,
+ },
+ },
+ ],
+} as TooltipInfo;
+
+export function MultipleInstances(props: TooltipInfo) {
+ return (
+
+ );
+}
+MultipleInstances.args = {
+ header: {
+ seriesIdentifier: {
+ key:
+ 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}',
+ specId: 'Instances',
+ yAccessor: '(index:0)',
+ splitAccessors: {},
+ seriesKeys: ['(index:0)'],
+ },
+ valueAccessor: 'y1',
+ label: 'Instances',
+ value: 9.606338858634443,
+ formattedValue: '9.606338858634443',
+ markValue: null,
+ color: '#6092c0',
+ isHighlighted: false,
+ isVisible: true,
+ datum: {
+ serviceNodeName:
+ '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f',
+ errorRate: 0.006896551724137931,
+ latency: 56465.53793103448,
+ throughput: 9.606338858634443,
+ cpuUsage: 0.0001,
+ memoryUsage: 0.1872131360014741,
+ },
+ },
+ values: [
+ {
+ seriesIdentifier: {
+ key:
+ 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}',
+ specId: 'Instances',
+ },
+ valueAccessor: 'y1',
+ label: 'Instances',
+ value: 56465.53793103448,
+ formattedValue: '56465.53793103448',
+ markValue: null,
+ color: '#6092c0',
+ isHighlighted: true,
+ isVisible: true,
+ datum: {
+ serviceNodeName:
+ '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f',
+ errorRate: 0.006896551724137931,
+ latency: 56465.53793103448,
+ throughput: 9.606338858634443,
+ cpuUsage: 0.0001,
+ memoryUsage: 0.1872131360014741,
+ },
+ },
+ {
+ seriesIdentifier: {
+ key:
+ 'groupId{__global__}spec{Instances}yAccessor{(index:0)}splitAccessors{}',
+ specId: 'Instances',
+ },
+ valueAccessor: 'y1',
+ label: 'Instances',
+ value: 56465.53793103448,
+ formattedValue: '56465.53793103448',
+ markValue: null,
+ color: '#6092c0',
+ isHighlighted: true,
+ isVisible: true,
+ datum: {
+ serviceNodeName:
+ '3b50ad269c45be69088905c4b355cc75ab94aaac1b35432bb752050438f4216f (2)',
+ errorRate: 0.006896551724137931,
+ latency: 56465.53793103448,
+ throughput: 9.606338858634443,
+ cpuUsage: 0.0001,
+ memoryUsage: 0.1872131360014741,
+ },
+ },
+ ],
+} as TooltipInfo;
diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx
new file mode 100644
index 00000000000000..2280fa91a659c0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/custom_tooltip.tsx
@@ -0,0 +1,214 @@
+/*
+ * 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; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { TooltipInfo } from '@elastic/charts';
+import { EuiIcon } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import { getServiceNodeName } from '../../../../../common/service_nodes';
+import {
+ asTransactionRate,
+ TimeFormatter,
+} from '../../../../../common/utils/formatters';
+import { useTheme } from '../../../../hooks/use_theme';
+import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table';
+
+const latencyLabel = i18n.translate(
+ 'xpack.apm.instancesLatencyDistributionChartTooltipLatencyLabel',
+ {
+ defaultMessage: 'Latency',
+ }
+);
+
+const throughputLabel = i18n.translate(
+ 'xpack.apm.instancesLatencyDistributionChartTooltipThroughputLabel',
+ {
+ defaultMessage: 'Throughput',
+ }
+);
+
+const clickToFilterDescription = i18n.translate(
+ 'xpack.apm.instancesLatencyDistributionChartTooltipClickToFilterDescription',
+ { defaultMessage: 'Click to filter by instance' }
+);
+
+/**
+ * Tooltip for a single instance
+ */
+function SingleInstanceCustomTooltip({
+ latencyFormatter,
+ values,
+}: {
+ latencyFormatter: TimeFormatter;
+ values: TooltipInfo['values'];
+}) {
+ const value = values[0];
+ const { color } = value;
+ const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem;
+ const { latency, serviceNodeName, throughput } = datum;
+
+ return (
+ <>
+
+ {getServiceNodeName(serviceNodeName)}
+
+
+
+
+
+ {latencyLabel}
+
+ {latencyFormatter(latency).formatted}
+
+
+
+
+
+
+ {throughputLabel}
+
+ {asTransactionRate(throughput)}
+
+
+
+
+ >
+ );
+}
+
+/**
+ * Tooltip for a multiple instances
+ */
+function MultipleInstanceCustomTooltip({
+ latencyFormatter,
+ values,
+}: TooltipInfo & { latencyFormatter: TimeFormatter }) {
+ const theme = useTheme();
+
+ return (
+ <>
+
+ {i18n.translate(
+ 'xpack.apm.instancesLatencyDistributionChartTooltipInstancesTitle',
+ {
+ defaultMessage:
+ '{instancesCount} {instancesCount, plural, one {instance} other {instances}}',
+ values: { instancesCount: values.length },
+ }
+ )}
+
+ {values.map((value) => {
+ const { color } = value;
+ const datum = (value.datum as unknown) as PrimaryStatsServiceInstanceItem;
+ const { latency, serviceNodeName, throughput } = datum;
+ return (
+
+
+
+
+
+ {getServiceNodeName(serviceNodeName)}
+
+
+
+
+
+
+ {latencyLabel}
+
+ {latencyFormatter(latency).formatted}
+
+
+
+
+
+
+ {throughputLabel}
+
+ {asTransactionRate(throughput)}
+
+
+
+
+ );
+ })}
+ >
+ );
+}
+
+/**
+ * Custom tooltip for instances latency distribution chart.
+ *
+ * The styling provided here recreates that in the Elastic Charts tooltip: https://github.com/elastic/elastic-charts/blob/58e6b5fbf77f4471d2a9a41c45a61f79ebd89b65/src/components/tooltip/tooltip.tsx
+ *
+ * We probably won't need to do all of this once https://github.com/elastic/elastic-charts/issues/615 is completed.
+ */
+export function CustomTooltip(
+ props: TooltipInfo & { latencyFormatter: TimeFormatter }
+) {
+ const { values } = props;
+ const theme = useTheme();
+
+ return (
+
+ {values.length > 1 ? (
+
+ ) : (
+
+ )}
+
+ {clickToFilterDescription}
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
index 5bcf0d161653ee..57ecbd4ca0b78b 100644
--- a/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
+++ b/x-pack/plugins/apm/public/components/shared/charts/instances_latency_distribution_chart/index.tsx
@@ -9,14 +9,21 @@ import {
Axis,
BubbleSeries,
Chart,
+ ElementClickListener,
+ GeometryValue,
Position,
ScaleType,
Settings,
+ TooltipInfo,
+ TooltipProps,
+ TooltipType,
} from '@elastic/charts';
import { EuiPanel, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
+import { useHistory } from 'react-router-dom';
import { useChartTheme } from '../../../../../../observability/public';
+import { SERVICE_NODE_NAME } from '../../../../../common/elasticsearch_fieldnames';
import {
asTransactionRate,
getDurationFormatter,
@@ -24,10 +31,12 @@ import {
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTheme } from '../../../../hooks/use_theme';
import { PrimaryStatsServiceInstanceItem } from '../../../app/service_overview/service_overview_instances_chart_and_table';
+import * as urlHelpers from '../../Links/url_helpers';
import { ChartContainer } from '../chart_container';
import { getResponseTimeTickFormatter } from '../transaction_charts/helper';
+import { CustomTooltip } from './custom_tooltip';
-interface InstancesLatencyDistributionChartProps {
+export interface InstancesLatencyDistributionChartProps {
height: number;
items?: PrimaryStatsServiceInstanceItem[];
status: FETCH_STATUS;
@@ -38,6 +47,7 @@ export function InstancesLatencyDistributionChart({
items = [],
status,
}: InstancesLatencyDistributionChartProps) {
+ const history = useHistory();
const hasData = items.length > 0;
const theme = useTheme();
@@ -51,6 +61,43 @@ export function InstancesLatencyDistributionChart({
const maxLatency = Math.max(...items.map((item) => item.latency ?? 0));
const latencyFormatter = getDurationFormatter(maxLatency);
+ const tooltip: TooltipProps = {
+ type: TooltipType.Follow,
+ snap: false,
+ customTooltip: (props: TooltipInfo) => (
+
+ ),
+ };
+
+ /**
+ * Handle click events on the items.
+ *
+ * Due to how we handle filtering by using the kuery bar, it's difficult to
+ * modify existing queries. If you have an existing query in the bar, this will
+ * wipe it out. This is ok for now, since we probably will be replacing this
+ * interaction with something nicer in a future release.
+ *
+ * The event object has an array two items for each point, one of which has
+ * the serviceNodeName, so we flatten the list and get the items we need to
+ * form a query.
+ */
+ const handleElementClick: ElementClickListener = (event) => {
+ const serviceNodeNamesQuery = event
+ .flat()
+ .flatMap((value) => (value as GeometryValue).datum?.serviceNodeName)
+ .filter((serviceNodeName) => !!serviceNodeName)
+ .map((serviceNodeName) => `${SERVICE_NODE_NAME}:"${serviceNodeName}"`)
+ .join(' OR ');
+
+ urlHelpers.push(history, { query: { kuery: serviceNodeNamesQuery } });
+ };
+
+ // With a linear scale, if all the instances have similar throughput (or if
+ // there's just a single instance) they'll show along the origin. Make sure
+ // the x-axis domain is [0, maxThroughput].
+ const maxThroughput = Math.max(...items.map((item) => item.throughput ?? 0));
+ const xDomain = { min: 0, max: maxThroughput };
+
return (
@@ -64,9 +111,11 @@ export function InstancesLatencyDistributionChart({
(
+
+
+
+ ),
+ ],
+};
+
+export function Example({ items }: InstancesLatencyDistributionChartProps) {
+ return (
+
+ );
+}
+Example.args = {
+ items: [
+ {
+ serviceNodeName:
+ '3f67bfc39c7891dc0c5657befb17bf58c19cf10f99472cf8df263c8e5bb1c766',
+ latency: 15802930.92133213,
+ throughput: 0.4019360641691481,
+ },
+ {
+ serviceNodeName:
+ 'd52c64bea9327f3e960ac1cb63c1b7ea922e3cb3d76ab9b254e57a7cb2f760a0',
+ latency: 8296442.578550679,
+ throughput: 0.3932978392703585,
+ },
+ {
+ serviceNodeName:
+ '797e0a906ad342223468ca51b663e1af8bdeb40bab376c46c7f7fa2021349290',
+ latency: 34842576.51204916,
+ throughput: 0.3353931699532713,
+ },
+ {
+ serviceNodeName:
+ '21e1c648bd73434a8a1bf6e849817930e8b43eacf73a5c39c30520ee3b79d8c0',
+ latency: 40713854.354498595,
+ throughput: 0.32947224189485164,
+ },
+ {
+ serviceNodeName:
+ 'a1c99c8675372af4c74bb01cc48e75989faa6f010a4ccb027df1c410dde0c72c',
+ latency: 18565471.348388012,
+ throughput: 0.3261219384041683,
+ },
+ {
+ serviceNodeName: '_service_node_name_missing_',
+ latency: 20065471.348388012,
+ throughput: 0.3261219384041683,
+ },
+ ],
+} as InstancesLatencyDistributionChartProps;
+
+export function SimilarThroughputInstances({
+ items,
+}: InstancesLatencyDistributionChartProps) {
+ return (
+
+ );
+}
+SimilarThroughputInstances.args = {
+ items: [
+ {
+ serviceNodeName:
+ '21e1c648bd73434a8a1bf6e849817930e8b43eacf73a5c39c30520ee3b79d8c0',
+ latency: 40713854.354498595,
+ throughput: 0.3261219384041683,
+ },
+ {
+ serviceNodeName:
+ 'a1c99c8675372af4c74bb01cc48e75989faa6f010a4ccb027df1c410dde0c72c',
+ latency: 18565471.348388012,
+ throughput: 0.3261219384041683,
+ },
+ {
+ serviceNodeName: '_service_node_name_missing_',
+ latency: 20065471.348388012,
+ throughput: 0.3261219384041683,
+ },
+ ],
+} as InstancesLatencyDistributionChartProps;