Skip to content

Commit

Permalink
adding elastic charts
Browse files Browse the repository at this point in the history
  • Loading branch information
cauemarcondes committed Oct 13, 2020
1 parent ce9ccc5 commit ee8ce4c
Show file tree
Hide file tree
Showing 14 changed files with 834 additions and 153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/

import {
Axis,
Chart,
HistogramBarSeries,
niceTimeFormatter,
Position,
ScaleType,
Settings,
SettingsSpec,
TooltipValue,
} from '@elastic/charts';

import { EuiTitle } from '@elastic/eui';
import theme from '@elastic/eui/dist/eui_theme_light.json';
import numeral from '@elastic/numeral';
import { i18n } from '@kbn/i18n';
import d3 from 'd3';
import { scaleUtc } from 'd3-scale';
import { mean } from 'lodash';
import React from 'react';
import { asRelativeDateTimeRange } from '../../../../../common/utils/formatters';
import { useTheme } from '../../../../hooks/useTheme';
import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs';
// @ts-expect-error
import Histogram from '../../../shared/charts/Histogram';
Expand Down Expand Up @@ -62,6 +74,7 @@ const tooltipHeader = (bucket: FormattedBucket) =>
asRelativeDateTimeRange(bucket.x0, bucket.x);

export function ErrorDistribution({ distribution, title }: Props) {
const theme = useTheme();
const buckets = getFormattedBuckets(
distribution.buckets,
distribution.bucketSize
Expand All @@ -82,48 +95,99 @@ export function ErrorDistribution({ distribution, title }: Props) {
const xMax = d3.max(buckets, (d) => d.x);
const tickFormat = scaleUtc().domain([xMin, xMax]).tickFormat();

const xFormatter = niceTimeFormatter([xMin, xMax]);

const tooltipProps: SettingsSpec['tooltip'] = {
headerFormatter: (tooltip: TooltipValue) => {
const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
if (serie) {
return asRelativeDateTimeRange(serie.x0, serie.x);
}
return `${tooltip.value}`;
},
};

return (
<div>
<EuiTitle size="xs">
<span>{title}</span>
</EuiTitle>
<Histogram
height={180}
noHits={distribution.noHits}
tooltipHeader={tooltipHeader}
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
xType="time-utc"
formatX={(value: Date) => {
const time = value.getTime();
return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
}}
buckets={buckets}
bucketSize={distribution.bucketSize}
formatYShort={(value: number) =>
i18n.translate('xpack.apm.errorGroupDetails.occurrencesShortLabel', {
defaultMessage: '{occCount} occ.',
values: { occCount: value },
})
}
formatYLong={(value: number) =>
i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
defaultMessage:
'{occCount} {occCount, plural, one {occurrence} other {occurrences}}',
values: { occCount: value },
})
}
legends={[
{
color: theme.euiColorVis1,
// 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
legendValue: numeral(averageValue).format('0a'),
title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
defaultMessage: 'Avg.',
}),
legendClickDisabled: true,
},
]}
/>
<div>
<Histogram
height={180}
noHits={distribution.noHits}
tooltipHeader={tooltipHeader}
verticalLineHover={(bucket: FormattedBucket) => bucket.x}
xType="time-utc"
formatX={(value: Date) => {
const time = value.getTime();
return tickFormat(new Date(time - getTimezoneOffsetInMs(time)));
}}
buckets={buckets}
bucketSize={distribution.bucketSize}
formatYShort={(value: number) =>
i18n.translate(
'xpack.apm.errorGroupDetails.occurrencesShortLabel',
{
defaultMessage: '{occCount} occ.',
values: { occCount: value },
}
)
}
formatYLong={(value: number) =>
i18n.translate('xpack.apm.errorGroupDetails.occurrencesLongLabel', {
defaultMessage:
'{occCount} {occCount, plural, one {occurrence} other {occurrences}}',
values: { occCount: value },
})
}
legends={[
{
color: theme.eui.euiColorVis1,
// 0a abbreviates large whole numbers with metric prefixes like: 1000 = 1k, 32000 = 32k, 1000000 = 1m
legendValue: numeral(averageValue).format('0a'),
title: i18n.translate('xpack.apm.errorGroupDetails.avgLabel', {
defaultMessage: 'Avg.',
}),
legendClickDisabled: true,
},
]}
/>
</div>
<div style={{ height: 180 }}>
<Chart>
<Settings
xDomain={{ min: xMin, max: xMax }}
tooltip={tooltipProps}
showLegend
showLegendExtra
legendPosition={Position.Bottom}
/>
<Axis
id="x-axis"
position={Position.Bottom}
showOverlappingTicks
tickFormat={xFormatter}
/>
<Axis
id="y-axis"
position={Position.Left}
ticks={2}
showGridLines
tickFormat={(value) => `${value} occ.`}
/>
<HistogramBarSeries
id="errorOccurrences"
name="Occurences"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x0"
yAccessors={['y']}
data={buckets}
color={theme.eui.euiColorVis1}
/>
</Chart>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,25 @@ import d3 from 'd3';
import { isEmpty } from 'lodash';
import React, { useCallback } from 'react';
import { ValuesType } from 'utility-types';
import {
Axis,
BarSeriesStyle,
Chart,
DataSeriesDatum,
ElementClickListener,
GeometryValue,
HistogramBarSeries,
PartialTheme,
Position,
RectAnnotation,
ScaleType,
Settings,
SettingsSpec,
TooltipValue,
} from '@elastic/charts';
import { getDurationFormatter } from '../../../../../common/utils/formatters';
import { useTheme } from '../../../../../../observability/public';
import { unit } from '../../../../style/variables';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { TransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
Expand Down Expand Up @@ -110,6 +128,7 @@ export function TransactionDistribution(props: Props) {
bucketIndex,
onBucketClick,
} = props;
const theme = useTheme();

/* eslint-disable-next-line react-hooks/exhaustive-deps */
const formatYShort = useCallback(getFormatYShort(transactionType), [
Expand Down Expand Up @@ -150,9 +169,34 @@ export function TransactionDistribution(props: Props) {
distribution.bucketSize
);

const xMin = d3.min(buckets, (d) => d.x0) || 0;
const xMax = d3.max(buckets, (d) => d.x) || 0;
const timeFormatter = getDurationFormatter(xMax);

const tooltipProps: SettingsSpec['tooltip'] = {
headerFormatter: (tooltip: TooltipValue) => {
const serie = buckets.find((bucket) => bucket.x0 === tooltip.value);
if (serie) {
const xFormatted = timeFormatter(serie.x);
const x0Formatted = timeFormatter(serie.x0);
return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
}
return `${timeFormatter(tooltip.value)}`;
},
};

const onBarClick: ElementClickListener = (elements) => {
const chartPoint = elements[0][0] as GeometryValue;
const clickedBucket = distribution?.buckets.find((bucket) => {
return bucket.key === chartPoint.x;
});
if (clickedBucket) {
onBucketClick(clickedBucket);
}
};

const selectedBucket = buckets[bucketIndex];

return (
<div>
<EuiTitle size="xs">
Expand Down Expand Up @@ -181,42 +225,85 @@ export function TransactionDistribution(props: Props) {
/>
</h5>
</EuiTitle>
<div style={{ height: unit * 10 }}>
<Histogram
buckets={buckets}
bucketSize={distribution.bucketSize}
bucketIndex={bucketIndex}
onClick={(chartPoint: IChartPoint) => {
const clickedBucket = getBucketFromChartPoint(chartPoint);

<Histogram
buckets={buckets}
bucketSize={distribution.bucketSize}
bucketIndex={bucketIndex}
onClick={(chartPoint: IChartPoint) => {
const clickedBucket = getBucketFromChartPoint(chartPoint);

if (clickedBucket) {
onBucketClick(clickedBucket);
}
}}
formatX={(time: number) => timeFormatter(time).formatted}
formatYShort={formatYShort}
formatYLong={formatYLong}
verticalLineHover={(point: IChartPoint) =>
isEmpty(getBucketFromChartPoint(point)?.samples)
}
backgroundHover={(point: IChartPoint) =>
!isEmpty(getBucketFromChartPoint(point)?.samples)
}
tooltipHeader={(point: IChartPoint) => {
const xFormatted = timeFormatter(point.x);
const x0Formatted = timeFormatter(point.x0);
return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
}}
tooltipFooter={(point: IChartPoint) =>
isEmpty(getBucketFromChartPoint(point)?.samples) &&
i18n.translate(
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip',
{
defaultMessage: 'No sample available for this bucket',
if (clickedBucket) {
onBucketClick(clickedBucket);
}
)
}
/>
}}
formatX={(time: number) => timeFormatter(time).formatted}
formatYShort={formatYShort}
formatYLong={formatYLong}
verticalLineHover={(point: IChartPoint) =>
isEmpty(getBucketFromChartPoint(point)?.samples)
}
backgroundHover={(point: IChartPoint) =>
!isEmpty(getBucketFromChartPoint(point)?.samples)
}
tooltipHeader={(point: IChartPoint) => {
const xFormatted = timeFormatter(point.x);
const x0Formatted = timeFormatter(point.x0);
return `${x0Formatted.value} - ${xFormatted.value} ${xFormatted.unit}`;
}}
tooltipFooter={(point: IChartPoint) =>
isEmpty(getBucketFromChartPoint(point)?.samples) &&
i18n.translate(
'xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip',
{
defaultMessage: 'No sample available for this bucket',
}
)
}
/>
</div>
<div style={{ height: unit * 10 }}>
<Chart>
<Settings
xDomain={{ min: xMin, max: xMax }}
tooltip={tooltipProps}
onElementClick={onBarClick}
/>
{selectedBucket && (
<RectAnnotation
id="highlighted_bucket"
dataValues={[
{
coordinates: { x0: selectedBucket.x0, x1: selectedBucket.x },
},
]}
style={{
fill: 'transparent',
strokeWidth: 1,
stroke: theme.eui.euiColorVis1,
opacity: 1,
}}
/>
)}
<Axis
id="bottom"
position={Position.Bottom}
showOverlappingTicks
tickFormat={(time: number) => timeFormatter(time).formatted}
/>
<Axis id="left" position={Position.Left} ticks={3} showGridLines />
<HistogramBarSeries
id="transactionDurationDistribution"
name="requests"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x0"
yAccessors={['y']}
data={buckets}
color={theme.eui.euiColorVis1}
/>
</Chart>
</div>
</div>
);
}
Loading

0 comments on commit ee8ce4c

Please sign in to comment.