mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [APM] Make sure stacked area charts handle no data points incorrectly Closes #40351. * Refactor CustomPlot/TransactionCharts - [x] Keep API calls in a separate file from the useTransactionBreakdown hook - [x] Bail out of render function early when `receivedDataDuringLifetime` is false - [x] Use `cluster` prop to draw lines over area instead of creating a new plot - [x] can lineseries be stacked without creating a new plot? - [x] Move functionality of SyncChartGroup to ChartsTimeContext (renamed to ChartsSyncContext) - [x] Only determine noHits inside of CustomPlot instead of using hits.total - [x] Remove use of getEmptySeries except for testing - [x] Send TimeSeries from the API instead of mapping data inside of the component - [x] Use default data in useTransactionBreakdown to prevent is-null checks - [x] Remove usage of start, end in CustomPlot * Additional cleanup on aisle five * Don't show GridLines or YAxis component if there is no data * Make formatters less defensive * Fix loading state for TransactionCharts * Update translation files
This commit is contained in:
parent
8767c1c10c
commit
7a5730639a
35 changed files with 490 additions and 793 deletions
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import { GenericMetricsChart } from '../../../../server/lib/metrics/transform_metrics_chart';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../../shared/charts/CustomPlot';
|
||||
import { HoverXHandlers } from '../../shared/charts/SyncChartGroup';
|
||||
import {
|
||||
asDynamicBytes,
|
||||
asPercent,
|
||||
|
@ -16,16 +15,16 @@ import {
|
|||
asDecimal
|
||||
} from '../../../utils/formatters';
|
||||
import { Coordinate } from '../../../../typings/timeseries';
|
||||
import { getEmptySeries } from '../../shared/charts/CustomPlot/getEmptySeries';
|
||||
import { isValidCoordinateValue } from '../../../utils/isValidCoordinateValue';
|
||||
import { useChartsSync } from '../../../hooks/useChartsSync';
|
||||
|
||||
interface Props {
|
||||
start: number | string | undefined;
|
||||
end: number | string | undefined;
|
||||
chart: GenericMetricsChart;
|
||||
hoverXHandlers: HoverXHandlers;
|
||||
}
|
||||
|
||||
export function MetricsChart({ start, end, chart, hoverXHandlers }: Props) {
|
||||
export function MetricsChart({ chart }: Props) {
|
||||
const formatYValue = getYTickFormatter(chart);
|
||||
const formatTooltip = getTooltipFormatter(chart);
|
||||
|
||||
|
@ -34,7 +33,7 @@ export function MetricsChart({ start, end, chart, hoverXHandlers }: Props) {
|
|||
legendValue: formatYValue(series.overallValue)
|
||||
}));
|
||||
|
||||
const noHits = chart.totalHits === 0;
|
||||
const syncedChartProps = useChartsSync();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -42,9 +41,8 @@ export function MetricsChart({ start, end, chart, hoverXHandlers }: Props) {
|
|||
<span>{chart.title}</span>
|
||||
</EuiTitle>
|
||||
<CustomPlot
|
||||
{...hoverXHandlers}
|
||||
noHits={noHits}
|
||||
series={noHits ? getEmptySeries(start, end) : transformedSeries}
|
||||
{...syncedChartProps}
|
||||
series={transformedSeries}
|
||||
tickFormatY={formatYValue}
|
||||
formatTooltipValue={formatTooltip}
|
||||
yMax={chart.yUnit === 'percent' ? 1 : 'max'}
|
||||
|
@ -64,10 +62,11 @@ function getYTickFormatter(chart: GenericMetricsChart) {
|
|||
return getFixedByteFormatter(max);
|
||||
}
|
||||
case 'percent': {
|
||||
return (y: number | null) => asPercent(y || 0, 1);
|
||||
return (y: number | null | undefined) => asPercent(y || 0, 1);
|
||||
}
|
||||
default: {
|
||||
return (y: number | null) => (y === null ? y : asDecimal(y));
|
||||
return (y: number | null | undefined) =>
|
||||
isValidCoordinateValue(y) ? asDecimal(y) : y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +80,8 @@ function getTooltipFormatter({ yUnit }: GenericMetricsChart) {
|
|||
return (c: Coordinate) => asPercent(c.y || 0, 1);
|
||||
}
|
||||
default: {
|
||||
return (c: Coordinate) => (c.y === null ? c.y : asDecimal(c.y));
|
||||
return (c: Coordinate) =>
|
||||
isValidCoordinateValue(c.y) ? asDecimal(c.y) : c.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
import { EuiFlexGrid, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
|
||||
import { SyncChartGroup } from '../../shared/charts/SyncChartGroup';
|
||||
import { MetricsChart } from './MetricsChart';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
|
||||
|
||||
interface ServiceMetricsProps {
|
||||
agentName?: string;
|
||||
|
@ -21,25 +21,17 @@ export function ServiceMetrics({ agentName }: ServiceMetricsProps) {
|
|||
const { start, end } = urlParams;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SyncChartGroup
|
||||
render={hoverXHandlers => (
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{data.charts.map(chart => (
|
||||
<EuiFlexItem key={chart.key}>
|
||||
<EuiPanel>
|
||||
<MetricsChart
|
||||
start={start}
|
||||
end={end}
|
||||
chart={chart}
|
||||
hoverXHandlers={hoverXHandlers}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
)}
|
||||
/>
|
||||
|
||||
<EuiFlexGrid columns={2} gutterSize="s">
|
||||
{data.charts.map(chart => (
|
||||
<EuiFlexItem key={chart.key}>
|
||||
<EuiPanel>
|
||||
<ChartsSyncContextProvider>
|
||||
<MetricsChart start={start} end={end} chart={chart} />
|
||||
</ChartsSyncContextProvider>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGrid>
|
||||
<EuiSpacer size="xxl" />
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { EuiPanel, EuiSpacer, EuiTitle, EuiHorizontalRule } from '@elastic/eui';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTransactionDetailsCharts } from '../../../hooks/useTransactionDetailsCharts';
|
||||
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
|
||||
import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution';
|
||||
import { useWaterfall } from '../../../hooks/useWaterfall';
|
||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||
|
@ -18,7 +18,7 @@ import { useLocation } from '../../../hooks/useLocation';
|
|||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { FETCH_STATUS } from '../../../hooks/useFetcher';
|
||||
import { TransactionBreakdown } from '../../shared/TransactionBreakdown';
|
||||
import { ChartsTimeContextProvider } from '../../../context/ChartsTimeContext';
|
||||
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
|
||||
|
||||
export function TransactionDetails() {
|
||||
const location = useLocation();
|
||||
|
@ -27,9 +27,9 @@ export function TransactionDetails() {
|
|||
data: distributionData,
|
||||
status: distributionStatus
|
||||
} = useTransactionDistribution(urlParams);
|
||||
const { data: transactionDetailsChartsData } = useTransactionDetailsCharts(
|
||||
urlParams
|
||||
);
|
||||
|
||||
const { data: transactionChartsData } = useTransactionCharts();
|
||||
|
||||
const { data: waterfall } = useWaterfall(urlParams);
|
||||
const transaction = waterfall.getTransactionById(urlParams.transactionId);
|
||||
|
||||
|
@ -43,18 +43,18 @@ export function TransactionDetails() {
|
|||
</EuiTitle>
|
||||
</ApmHeader>
|
||||
|
||||
<ChartsTimeContextProvider>
|
||||
<ChartsSyncContextProvider>
|
||||
<TransactionBreakdown />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionCharts
|
||||
hasMLJob={false}
|
||||
charts={transactionDetailsChartsData}
|
||||
charts={transactionChartsData}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
</ChartsTimeContextProvider>
|
||||
</ChartsSyncContextProvider>
|
||||
|
||||
<EuiHorizontalRule size="full" margin="l" />
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Location } from 'history';
|
|||
import { first } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTransactionList } from '../../../hooks/useTransactionList';
|
||||
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
|
||||
import { useTransactionCharts } from '../../../hooks/useTransactionCharts';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||
import { TransactionBreakdown } from '../../shared/TransactionBreakdown';
|
||||
|
@ -27,7 +27,7 @@ import { useFetcher } from '../../../hooks/useFetcher';
|
|||
import { getHasMLJob } from '../../../services/rest/ml';
|
||||
import { history } from '../../../utils/history';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { ChartsTimeContextProvider } from '../../../context/ChartsTimeContext';
|
||||
import { ChartsSyncContextProvider } from '../../../context/ChartsSyncContext';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
|
@ -70,9 +70,7 @@ export function TransactionOverview({
|
|||
})
|
||||
);
|
||||
|
||||
const { data: transactionOverviewCharts } = useTransactionOverviewCharts(
|
||||
urlParams
|
||||
);
|
||||
const { data: transactionCharts } = useTransactionCharts();
|
||||
|
||||
// TODO: improve urlParams typings.
|
||||
// `serviceName` or `transactionType` will never be undefined here, and this check should not be needed
|
||||
|
@ -115,18 +113,18 @@ export function TransactionOverview({
|
|||
</EuiFormRow>
|
||||
) : null}
|
||||
|
||||
<ChartsTimeContextProvider>
|
||||
<ChartsSyncContextProvider>
|
||||
<TransactionBreakdown initialIsOpen={true} />
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionCharts
|
||||
hasMLJob={hasMLJob}
|
||||
charts={transactionOverviewCharts}
|
||||
charts={transactionCharts}
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</ChartsTimeContextProvider>
|
||||
</ChartsSyncContextProvider>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
|
|
|
@ -4,57 +4,40 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
|
||||
import { TransactionLineChart } from '../../charts/TransactionCharts/TransactionLineChart';
|
||||
import { asPercent } from '../../../../utils/formatters';
|
||||
import { unit } from '../../../../style/variables';
|
||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||
|
||||
interface Props {
|
||||
timeseries: Array<{
|
||||
name: string;
|
||||
color: string;
|
||||
values: Array<{ x: number; y: number | null }>;
|
||||
}>;
|
||||
timeseries: TimeSeries[];
|
||||
}
|
||||
|
||||
const tickFormatY = (y: number | null | undefined) => {
|
||||
return numeral(y || 0).format('0 %');
|
||||
};
|
||||
|
||||
const formatTooltipValue = (coordinate: Coordinate) => {
|
||||
return isValidCoordinateValue(coordinate.y)
|
||||
? asPercent(coordinate.y, 1)
|
||||
: NOT_AVAILABLE_LABEL;
|
||||
};
|
||||
|
||||
const TransactionBreakdownGraph: React.FC<Props> = props => {
|
||||
const { timeseries } = props;
|
||||
|
||||
const series: React.ComponentProps<
|
||||
typeof TransactionLineChart
|
||||
>['series'] = useMemo(() => {
|
||||
return timeseries.map(timeseriesConfig => {
|
||||
return {
|
||||
title: timeseriesConfig.name,
|
||||
color: timeseriesConfig.color,
|
||||
data: timeseriesConfig.values,
|
||||
type: 'area',
|
||||
hideLegend: true
|
||||
};
|
||||
}, {});
|
||||
}, [timeseries]);
|
||||
|
||||
const tickFormatY = useCallback((y: number | null) => {
|
||||
return numeral(y || 0).format('0 %');
|
||||
}, []);
|
||||
|
||||
const formatTooltipValue = useCallback((coordinate: Coordinate) => {
|
||||
return coordinate.y !== null && coordinate.y !== undefined
|
||||
? asPercent(coordinate.y, 1)
|
||||
: NOT_AVAILABLE_LABEL;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TransactionLineChart
|
||||
series={series}
|
||||
stacked={true}
|
||||
series={timeseries}
|
||||
tickFormatY={tickFormatY}
|
||||
formatTooltipValue={formatTooltipValue}
|
||||
yMax={1}
|
||||
height={unit * 12}
|
||||
stacked={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -20,19 +19,6 @@ import { TransactionBreakdownKpiList } from './TransactionBreakdownKpiList';
|
|||
import { TransactionBreakdownGraph } from './TransactionBreakdownGraph';
|
||||
import { FETCH_STATUS } from '../../../hooks/useFetcher';
|
||||
|
||||
const COLORS = [
|
||||
theme.euiColorVis0,
|
||||
theme.euiColorVis1,
|
||||
theme.euiColorVis2,
|
||||
theme.euiColorVis3,
|
||||
theme.euiColorVis4,
|
||||
theme.euiColorVis5,
|
||||
theme.euiColorVis6,
|
||||
theme.euiColorVis7,
|
||||
theme.euiColorVis8,
|
||||
theme.euiColorVis9
|
||||
];
|
||||
|
||||
const NoTransactionsTitle = styled.span`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
@ -48,58 +34,17 @@ const TransactionBreakdown: React.FC<{
|
|||
receivedDataDuringLifetime
|
||||
} = useTransactionBreakdown();
|
||||
|
||||
const kpis = data ? data.kpis : undefined;
|
||||
const timeseriesPerSubtype = data ? data.timeseries_per_subtype : undefined;
|
||||
|
||||
const legends = useMemo(() => {
|
||||
const names = kpis ? kpis.map(kpi => kpi.name).sort() : [];
|
||||
|
||||
return names.map((name, index) => {
|
||||
return {
|
||||
name,
|
||||
color: COLORS[index % COLORS.length]
|
||||
};
|
||||
});
|
||||
}, [kpis]);
|
||||
|
||||
const sortedAndColoredKpis = useMemo(() => {
|
||||
if (!kpis) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return legends.map(legend => {
|
||||
const { color } = legend;
|
||||
|
||||
const breakdown = kpis.find(
|
||||
b => b.name === legend.name
|
||||
) as typeof kpis[0];
|
||||
|
||||
return {
|
||||
...breakdown,
|
||||
color
|
||||
};
|
||||
});
|
||||
}, [kpis, legends]);
|
||||
|
||||
const loading = status === FETCH_STATUS.LOADING || status === undefined;
|
||||
|
||||
const hasHits = data && data.kpis.length > 0;
|
||||
const timeseries = useMemo(() => {
|
||||
if (!timeseriesPerSubtype) {
|
||||
return [];
|
||||
}
|
||||
return legends.map(legend => {
|
||||
const series = timeseriesPerSubtype[legend.name];
|
||||
const { kpis, timeseries } = data;
|
||||
|
||||
return {
|
||||
name: legend.name,
|
||||
values: series,
|
||||
color: legend.color
|
||||
};
|
||||
});
|
||||
}, [timeseriesPerSubtype, legends]);
|
||||
const hasHits = kpis.length > 0;
|
||||
|
||||
return receivedDataDuringLifetime ? (
|
||||
if (!receivedDataDuringLifetime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -111,11 +56,9 @@ const TransactionBreakdown: React.FC<{
|
|||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{hasHits && sortedAndColoredKpis ? (
|
||||
{hasHits ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
{sortedAndColoredKpis && (
|
||||
<TransactionBreakdownKpiList kpis={sortedAndColoredKpis} />
|
||||
)}
|
||||
<TransactionBreakdownKpiList kpis={kpis} />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
!loading && (
|
||||
|
@ -155,7 +98,7 @@ const TransactionBreakdown: React.FC<{
|
|||
) : null}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export { TransactionBreakdown };
|
||||
|
|
|
@ -21,8 +21,14 @@ import { rgba } from 'polished';
|
|||
import StatusText from './StatusText';
|
||||
import { SharedPlot } from './plotUtils';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||
|
||||
// undefined values are converted by react-vis into NaN when stacking
|
||||
// see https://github.com/uber/react-vis/issues/1214
|
||||
const getNull = d => isValidCoordinateValue(d.y) && !isNaN(d.y);
|
||||
|
||||
const X_TICK_TOTAL = 7;
|
||||
|
||||
class StaticPlot extends PureComponent {
|
||||
getVisSeries(series, plotValues) {
|
||||
return series
|
||||
|
@ -36,7 +42,7 @@ class StaticPlot extends PureComponent {
|
|||
case 'line':
|
||||
return (
|
||||
<LineSeries
|
||||
getNull={d => d.y !== null}
|
||||
getNull={getNull}
|
||||
key={serie.title}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
|
@ -48,17 +54,54 @@ class StaticPlot extends PureComponent {
|
|||
case 'area':
|
||||
return (
|
||||
<AreaSeries
|
||||
getNull={d => d.y !== null}
|
||||
getNull={getNull}
|
||||
key={serie.title}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
data={serie.data}
|
||||
color={serie.color}
|
||||
stroke={serie.stack ? 'rgba(0,0,0,0)' : serie.color}
|
||||
stroke={serie.color}
|
||||
fill={serie.areaColor || rgba(serie.color, 0.3)}
|
||||
stack={serie.stack}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'areaStacked': {
|
||||
// convert null into undefined because of stack issues,
|
||||
// see https://github.com/uber/react-vis/issues/1214
|
||||
const data = serie.data.map(value => {
|
||||
return 'y' in value && isValidCoordinateValue(value.y)
|
||||
? value
|
||||
: {
|
||||
...value,
|
||||
y: undefined
|
||||
};
|
||||
});
|
||||
return [
|
||||
<AreaSeries
|
||||
getNull={getNull}
|
||||
key={`${serie.title}-area`}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
data={data}
|
||||
color={serie.color}
|
||||
stroke={'rgba(0,0,0,0)'}
|
||||
fill={serie.areaColor || rgba(serie.color, 0.3)}
|
||||
stack={true}
|
||||
cluster="area"
|
||||
/>,
|
||||
<LineSeries
|
||||
getNull={getNull}
|
||||
key={`${serie.title}-line`}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
data={data}
|
||||
color={serie.color}
|
||||
stack={true}
|
||||
cluster="line"
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
||||
case 'areaMaxHeight':
|
||||
const yMax = last(plotValues.yTickValues);
|
||||
const data = serie.data.map(p => ({
|
||||
|
@ -70,7 +113,7 @@ class StaticPlot extends PureComponent {
|
|||
|
||||
return (
|
||||
<VerticalRectSeries
|
||||
getNull={d => d.y !== null}
|
||||
getNull={getNull}
|
||||
key={serie.title}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
|
@ -83,7 +126,7 @@ class StaticPlot extends PureComponent {
|
|||
case 'linemark':
|
||||
return (
|
||||
<LineMarkSeries
|
||||
getNull={d => d.y !== null}
|
||||
getNull={getNull}
|
||||
key={serie.title}
|
||||
xType="time"
|
||||
curve={'curveMonotoneX'}
|
||||
|
@ -103,17 +146,7 @@ class StaticPlot extends PureComponent {
|
|||
|
||||
return (
|
||||
<SharedPlot plotValues={plotValues}>
|
||||
<HorizontalGridLines tickValues={yTickValues} />
|
||||
<XAxis tickSize={0} tickTotal={X_TICK_TOTAL} tickFormat={tickFormatX} />
|
||||
<YAxis
|
||||
tickSize={0}
|
||||
tickValues={yTickValues}
|
||||
tickFormat={tickFormatY}
|
||||
style={{
|
||||
line: { stroke: 'none', fill: 'none' }
|
||||
}}
|
||||
/>
|
||||
|
||||
{noHits ? (
|
||||
<StatusText
|
||||
marginLeft={30}
|
||||
|
@ -122,7 +155,19 @@ class StaticPlot extends PureComponent {
|
|||
})}
|
||||
/>
|
||||
) : (
|
||||
this.getVisSeries(series, plotValues)
|
||||
[
|
||||
<HorizontalGridLines key="grid-lines" tickValues={yTickValues} />,
|
||||
<YAxis
|
||||
key="y-axis"
|
||||
tickSize={0}
|
||||
tickValues={yTickValues}
|
||||
tickFormat={tickFormatY}
|
||||
style={{
|
||||
line: { stroke: 'none', fill: 'none' }
|
||||
}}
|
||||
/>,
|
||||
this.getVisSeries(series, plotValues)
|
||||
]
|
||||
)}
|
||||
</SharedPlot>
|
||||
);
|
||||
|
|
|
@ -4,27 +4,27 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { memoize } from 'lodash';
|
||||
import d3 from 'd3';
|
||||
|
||||
export const getEmptySeries = memoize(
|
||||
(
|
||||
start: number | string = Date.now() - 3600000,
|
||||
end: number | string = Date.now()
|
||||
) => {
|
||||
const dates = d3.time
|
||||
.scale()
|
||||
.domain([new Date(start), new Date(end)])
|
||||
.ticks();
|
||||
export const getEmptySeries = (
|
||||
start = Date.now() - 3600000,
|
||||
end = Date.now()
|
||||
) => {
|
||||
const dates = d3.time
|
||||
.scale()
|
||||
.domain([new Date(start), new Date(end)])
|
||||
.ticks();
|
||||
|
||||
return [
|
||||
{
|
||||
data: dates.map(x => ({
|
||||
x: x.getTime(),
|
||||
y: 1
|
||||
}))
|
||||
}
|
||||
];
|
||||
},
|
||||
(start: string, end: string) => [start, end].join('_')
|
||||
);
|
||||
return [
|
||||
{
|
||||
title: '',
|
||||
type: 'line',
|
||||
legendValue: '',
|
||||
color: '',
|
||||
data: dates.map(x => ({
|
||||
x: x.getTime(),
|
||||
y: null
|
||||
}))
|
||||
}
|
||||
];
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { isEmpty } from 'lodash';
|
||||
import { isEmpty, flatten } from 'lodash';
|
||||
import { makeWidthFlexible } from 'react-vis';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent, Fragment } from 'react';
|
||||
|
@ -15,6 +15,7 @@ import InteractivePlot from './InteractivePlot';
|
|||
import VoronoiPlot from './VoronoiPlot';
|
||||
import { createSelector } from 'reselect';
|
||||
import { getPlotValues } from './plotUtils';
|
||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||
|
||||
const VISIBLE_LEGEND_COUNT = 4;
|
||||
|
||||
|
@ -33,31 +34,22 @@ export class InnerCustomPlot extends PureComponent {
|
|||
getEnabledSeries = createSelector(
|
||||
state => state.visibleSeries,
|
||||
state => state.seriesEnabledState,
|
||||
state => state.stacked,
|
||||
(visibleSeries, seriesEnabledState, stacked) =>
|
||||
visibleSeries
|
||||
.filter((serie, i) => !seriesEnabledState[i])
|
||||
.map(serie => {
|
||||
return stacked ? { ...serie, stack: true } : serie;
|
||||
})
|
||||
(visibleSeries, seriesEnabledState) =>
|
||||
visibleSeries.filter((serie, i) => !seriesEnabledState[i])
|
||||
);
|
||||
|
||||
getOptions = createSelector(
|
||||
state => state.width,
|
||||
state => state.yMin,
|
||||
state => state.yMax,
|
||||
state => state.start,
|
||||
state => state.end,
|
||||
state => state.height,
|
||||
state => state.stacked,
|
||||
(width, yMin, yMax, start, end, height, stacked) => ({
|
||||
state => state.stackBy,
|
||||
(width, yMin, yMax, height, stackBy) => ({
|
||||
width,
|
||||
yMin,
|
||||
yMax,
|
||||
start,
|
||||
end,
|
||||
height,
|
||||
stacked
|
||||
stackBy
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -78,18 +70,6 @@ export class InnerCustomPlot extends PureComponent {
|
|||
}
|
||||
);
|
||||
|
||||
getStackedPlotSeries = createSelector(
|
||||
state => state.enabledSeries,
|
||||
series => {
|
||||
return series.map(serie => {
|
||||
return {
|
||||
...serie,
|
||||
type: 'line'
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
clickLegend = i => {
|
||||
this.setState(({ seriesEnabledState }) => {
|
||||
const nextSeriesEnabledState = this.props.series.map((value, _i) => {
|
||||
|
@ -142,9 +122,9 @@ export class InnerCustomPlot extends PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { series, truncateLegends, noHits, width, stacked } = this.props;
|
||||
const { series, truncateLegends, width } = this.props;
|
||||
|
||||
if (isEmpty(series) || !width) {
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -155,14 +135,16 @@ export class InnerCustomPlot extends PureComponent {
|
|||
const visibleSeries = this.getVisibleSeries({ series });
|
||||
const enabledSeries = this.getEnabledSeries({
|
||||
visibleSeries,
|
||||
seriesEnabledState: this.state.seriesEnabledState,
|
||||
stacked
|
||||
seriesEnabledState: this.state.seriesEnabledState
|
||||
});
|
||||
const options = this.getOptions(this.props);
|
||||
|
||||
const coordinates = flatten(enabledSeries.map(s => s.data));
|
||||
const noHits = coordinates.every(coord => !isValidCoordinateValue(coord.y));
|
||||
|
||||
const plotValues = this.getPlotValues({
|
||||
visibleSeries,
|
||||
enabledSeries,
|
||||
enabledSeries: enabledSeries,
|
||||
options
|
||||
});
|
||||
|
||||
|
@ -170,16 +152,6 @@ export class InnerCustomPlot extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const staticPlot = (
|
||||
<StaticPlot
|
||||
noHits={noHits}
|
||||
plotValues={plotValues}
|
||||
series={enabledSeries}
|
||||
tickFormatY={this.props.tickFormatY}
|
||||
tickFormatX={this.props.tickFormatX}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div style={{ position: 'relative', height: plotValues.XY_HEIGHT }}>
|
||||
|
@ -191,12 +163,6 @@ export class InnerCustomPlot extends PureComponent {
|
|||
tickFormatX={this.props.tickFormatX}
|
||||
/>
|
||||
|
||||
{stacked
|
||||
? React.cloneElement(staticPlot, {
|
||||
series: this.getStackedPlotSeries({ enabledSeries })
|
||||
})
|
||||
: null}
|
||||
|
||||
<InteractivePlot
|
||||
plotValues={plotValues}
|
||||
hoverX={this.props.hoverX}
|
||||
|
@ -232,7 +198,6 @@ export class InnerCustomPlot extends PureComponent {
|
|||
InnerCustomPlot.propTypes = {
|
||||
formatTooltipValue: PropTypes.func,
|
||||
hoverX: PropTypes.number,
|
||||
noHits: PropTypes.bool.isRequired,
|
||||
onHover: PropTypes.func.isRequired,
|
||||
onMouseLeave: PropTypes.func.isRequired,
|
||||
onSelectionEnd: PropTypes.func.isRequired,
|
||||
|
@ -240,16 +205,15 @@ InnerCustomPlot.propTypes = {
|
|||
tickFormatY: PropTypes.func,
|
||||
truncateLegends: PropTypes.bool,
|
||||
width: PropTypes.number.isRequired,
|
||||
stacked: PropTypes.bool,
|
||||
height: PropTypes.number
|
||||
height: PropTypes.number,
|
||||
stackBy: PropTypes.string
|
||||
};
|
||||
|
||||
InnerCustomPlot.defaultProps = {
|
||||
formatTooltipValue: p => p.y,
|
||||
tickFormatX: undefined,
|
||||
tickFormatY: y => y,
|
||||
truncateLegends: false,
|
||||
stacked: false
|
||||
truncateLegends: false
|
||||
};
|
||||
|
||||
export default makeWidthFlexible(InnerCustomPlot);
|
||||
|
|
|
@ -46,19 +46,16 @@ function getFlattenedCoordinates(visibleSeries, enabledSeries) {
|
|||
export function getPlotValues(
|
||||
visibleSeries,
|
||||
enabledSeries,
|
||||
{ width, yMin = 0, yMax = 'max', start, end, height, stacked }
|
||||
{ width, yMin = 0, yMax = 'max', height, stackBy }
|
||||
) {
|
||||
const flattenedCoordinates = getFlattenedCoordinates(
|
||||
visibleSeries,
|
||||
enabledSeries
|
||||
);
|
||||
if (isEmpty(flattenedCoordinates)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const xMin = start ? start : d3.min(flattenedCoordinates, d => d.x);
|
||||
const xMin = d3.min(flattenedCoordinates, d => d.x);
|
||||
|
||||
const xMax = end ? end : d3.max(flattenedCoordinates, d => d.x);
|
||||
const xMax = d3.max(flattenedCoordinates, d => d.x);
|
||||
|
||||
if (yMax === 'max') {
|
||||
yMax = d3.max(flattenedCoordinates, d => d.y);
|
||||
|
@ -80,7 +77,7 @@ export function getPlotValues(
|
|||
XY_MARGIN,
|
||||
XY_HEIGHT: height || XY_HEIGHT,
|
||||
XY_WIDTH: width,
|
||||
...(stacked ? { stackBy: 'y' } : {})
|
||||
stackBy
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { mount } from 'enzyme';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
|
||||
import { toJson } from '../../../../../utils/testHelpers';
|
||||
import { InnerCustomPlot } from '../index';
|
||||
import responseWithData from './responseWithData.json';
|
||||
|
@ -33,7 +32,6 @@ describe('when response has data', () => {
|
|||
onSelectionEnd = jest.fn();
|
||||
wrapper = mount(
|
||||
<InnerCustomPlot
|
||||
noHits={false}
|
||||
series={series}
|
||||
onHover={onHover}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
@ -291,7 +289,6 @@ describe('when response has no data', () => {
|
|||
|
||||
wrapper = mount(
|
||||
<InnerCustomPlot
|
||||
noHits={true}
|
||||
series={series}
|
||||
onHover={onHover}
|
||||
onMouseLeave={onMouseLeave}
|
||||
|
@ -333,8 +330,8 @@ describe('when response has no data', () => {
|
|||
expect(wrapper.prop('series').length).toBe(1);
|
||||
});
|
||||
|
||||
it('The series is empty and every y-value is 1', () => {
|
||||
expect(wrapper.prop('series')[0].data.every(d => d.y === 1)).toEqual(
|
||||
it('The series is empty and every y-value is null', () => {
|
||||
expect(wrapper.prop('series')[0].data.every(d => d.y === null)).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
|
|
@ -72,32 +72,6 @@ Array [
|
|||
onWheel={[Function]}
|
||||
width={800}
|
||||
>
|
||||
<g
|
||||
className="rv-xy-plot__grid-lines"
|
||||
transform="translate(80,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={208}
|
||||
y2={208}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={104}
|
||||
y2={104}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--horizontal "
|
||||
style={Object {}}
|
||||
|
@ -255,6 +229,32 @@ Array [
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__grid-lines"
|
||||
transform="translate(80,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={208}
|
||||
y2={208}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={104}
|
||||
y2={104}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--vertical "
|
||||
style={
|
||||
|
@ -2949,32 +2949,6 @@ Array [
|
|||
onWheel={[Function]}
|
||||
width={800}
|
||||
>
|
||||
<g
|
||||
className="rv-xy-plot__grid-lines"
|
||||
transform="translate(80,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={208}
|
||||
y2={208}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={104}
|
||||
y2={104}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--horizontal "
|
||||
style={Object {}}
|
||||
|
@ -3132,6 +3106,32 @@ Array [
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__grid-lines"
|
||||
transform="translate(80,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={208}
|
||||
y2={208}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={104}
|
||||
y2={104}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={720}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--vertical "
|
||||
style={
|
||||
|
@ -5852,32 +5852,6 @@ Array [
|
|||
onWheel={[Function]}
|
||||
width={100}
|
||||
>
|
||||
<g
|
||||
className="rv-xy-plot__grid-lines"
|
||||
transform="translate(80,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={20}
|
||||
y1={208}
|
||||
y2={208}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={20}
|
||||
y1={104}
|
||||
y2={104}
|
||||
/>
|
||||
<line
|
||||
className="rv-xy-plot__grid-lines__line"
|
||||
x1={0}
|
||||
x2={20}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--horizontal "
|
||||
style={Object {}}
|
||||
|
@ -6196,179 +6170,6 @@ Array [
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis rv-xy-plot__axis--vertical "
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
transform="translate(0,16)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__axis__line"
|
||||
style={
|
||||
Object {
|
||||
"fill": "none",
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
"stroke": "none",
|
||||
}
|
||||
}
|
||||
x1={80}
|
||||
x2={80}
|
||||
y1={0}
|
||||
y2={208}
|
||||
/>
|
||||
<g
|
||||
className="rv-xy-plot__axis__ticks"
|
||||
transform="translate(80, 0)"
|
||||
>
|
||||
<g
|
||||
className="rv-xy-plot__axis__tick"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
transform="translate(0, 208)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__axis__tick__line"
|
||||
style={
|
||||
Object {
|
||||
"fill": "none",
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
"stroke": "none",
|
||||
}
|
||||
}
|
||||
x1={0}
|
||||
x2={-0}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
<text
|
||||
className="rv-xy-plot__axis__tick__text"
|
||||
dy="0.32em"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
textAnchor="end"
|
||||
transform="translate(-8, 0)"
|
||||
>
|
||||
0
|
||||
</text>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis__tick"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
transform="translate(0, 104)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__axis__tick__line"
|
||||
style={
|
||||
Object {
|
||||
"fill": "none",
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
"stroke": "none",
|
||||
}
|
||||
}
|
||||
x1={0}
|
||||
x2={-0}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
<text
|
||||
className="rv-xy-plot__axis__tick__text"
|
||||
dy="0.32em"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
textAnchor="end"
|
||||
transform="translate(-8, 0)"
|
||||
>
|
||||
0.5
|
||||
</text>
|
||||
</g>
|
||||
<g
|
||||
className="rv-xy-plot__axis__tick"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
transform="translate(0, 0)"
|
||||
>
|
||||
<line
|
||||
className="rv-xy-plot__axis__tick__line"
|
||||
style={
|
||||
Object {
|
||||
"fill": "none",
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
"stroke": "none",
|
||||
}
|
||||
}
|
||||
x1={0}
|
||||
x2={-0}
|
||||
y1={0}
|
||||
y2={0}
|
||||
/>
|
||||
<text
|
||||
className="rv-xy-plot__axis__tick__text"
|
||||
dy="0.32em"
|
||||
style={
|
||||
Object {
|
||||
"line": Object {
|
||||
"fill": "none",
|
||||
"stroke": "none",
|
||||
},
|
||||
}
|
||||
}
|
||||
textAnchor="end"
|
||||
transform="translate(-8, 0)"
|
||||
>
|
||||
1
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<div
|
||||
style={
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fromQuery, toQuery } from '../../Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
|
||||
export interface RangeSelection {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
type HoverX = number;
|
||||
type OnHoverHandler = (hoverX: HoverX) => void;
|
||||
type OnMouseLeaveHandler = () => void;
|
||||
type OnSelectionEndHandler = (range: RangeSelection) => void;
|
||||
|
||||
export interface HoverXHandlers {
|
||||
onHover: OnHoverHandler;
|
||||
onMouseLeave: OnMouseLeaveHandler;
|
||||
onSelectionEnd: OnSelectionEndHandler;
|
||||
hoverX: HoverX | null;
|
||||
}
|
||||
|
||||
interface SyncChartGroupProps {
|
||||
render: (props: HoverXHandlers) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface SyncChartState {
|
||||
hoverX: HoverX | null;
|
||||
}
|
||||
|
||||
export class SyncChartGroup extends React.Component<
|
||||
SyncChartGroupProps,
|
||||
SyncChartState
|
||||
> {
|
||||
public state = { hoverX: null };
|
||||
|
||||
public onHover: OnHoverHandler = hoverX => this.setState({ hoverX });
|
||||
public onMouseLeave: OnMouseLeaveHandler = () =>
|
||||
this.setState({ hoverX: null });
|
||||
public onSelectionEnd: OnSelectionEndHandler = range => {
|
||||
this.setState({ hoverX: null });
|
||||
|
||||
const currentSearch = toQuery(history.location.search);
|
||||
const nextSearch = {
|
||||
rangeFrom: new Date(range.start).toISOString(),
|
||||
rangeTo: new Date(range.end).toISOString()
|
||||
};
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...currentSearch,
|
||||
...nextSearch
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
public render() {
|
||||
return this.props.render({
|
||||
onHover: this.onHover,
|
||||
onMouseLeave: this.onMouseLeave,
|
||||
onSelectionEnd: this.onSelectionEnd,
|
||||
hoverX: this.state.hoverX
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,19 +4,14 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
import d3 from 'd3';
|
||||
import React from 'react';
|
||||
import {
|
||||
Coordinate,
|
||||
RectCoordinate
|
||||
} from '../../../../../../typings/timeseries';
|
||||
import { useChartsTime } from '../../../../../hooks/useChartsTime';
|
||||
import { getEmptySeries } from '../../CustomPlot/getEmptySeries';
|
||||
import { useChartsSync } from '../../../../../hooks/useChartsSync';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../../CustomPlot';
|
||||
import { toQuery, fromQuery } from '../../../Links/url_helpers';
|
||||
import { history } from '../../../../../utils/history';
|
||||
|
||||
interface Props {
|
||||
series: Array<{
|
||||
|
@ -26,12 +21,12 @@ interface Props {
|
|||
data: Array<Coordinate | RectCoordinate>;
|
||||
type: string;
|
||||
}>;
|
||||
stacked?: boolean;
|
||||
truncateLegends?: boolean;
|
||||
tickFormatY: (y: number | null) => React.ReactNode;
|
||||
tickFormatY: (y: number) => React.ReactNode;
|
||||
formatTooltipValue: (c: Coordinate) => React.ReactNode;
|
||||
yMax?: string | number;
|
||||
height?: number;
|
||||
stacked?: boolean;
|
||||
}
|
||||
|
||||
const TransactionLineChart: React.FC<Props> = (props: Props) => {
|
||||
|
@ -39,69 +34,24 @@ const TransactionLineChart: React.FC<Props> = (props: Props) => {
|
|||
series,
|
||||
tickFormatY,
|
||||
formatTooltipValue,
|
||||
stacked = false,
|
||||
yMax = 'max',
|
||||
height,
|
||||
truncateLegends
|
||||
truncateLegends,
|
||||
stacked = false
|
||||
} = props;
|
||||
|
||||
const flattenedCoordinates = flatten(series.map(serie => serie.data));
|
||||
|
||||
const start = d3.min(flattenedCoordinates, d => d.x);
|
||||
const end = d3.max(flattenedCoordinates, d => d.x);
|
||||
|
||||
const noHits = series.every(
|
||||
serie =>
|
||||
serie.data.filter(value => 'y' in value && value.y !== null).length === 0
|
||||
);
|
||||
const { time, setTime } = useChartsTime();
|
||||
|
||||
const hoverXHandlers = useMemo(() => {
|
||||
return {
|
||||
onHover: (hoverX: number) => {
|
||||
setTime(hoverX);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setTime(null);
|
||||
},
|
||||
onSelectionEnd: (range: { start: number; end: number }) => {
|
||||
setTime(null);
|
||||
|
||||
const currentSearch = toQuery(history.location.search);
|
||||
const nextSearch = {
|
||||
rangeFrom: new Date(range.start).toISOString(),
|
||||
rangeTo: new Date(range.end).toISOString()
|
||||
};
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...currentSearch,
|
||||
...nextSearch
|
||||
})
|
||||
});
|
||||
},
|
||||
hoverX: time
|
||||
};
|
||||
}, [time, setTime]);
|
||||
|
||||
if (!start || !end) {
|
||||
return null;
|
||||
}
|
||||
const syncedChartsProps = useChartsSync();
|
||||
|
||||
return (
|
||||
<CustomPlot
|
||||
noHits={noHits}
|
||||
series={noHits ? getEmptySeries(start, end) : series}
|
||||
start={new Date(start).getTime()}
|
||||
end={new Date(end).getTime()}
|
||||
{...hoverXHandlers}
|
||||
series={series}
|
||||
{...syncedChartsProps}
|
||||
tickFormatY={tickFormatY}
|
||||
formatTooltipValue={formatTooltipValue}
|
||||
stacked={stacked}
|
||||
yMax={yMax}
|
||||
height={height}
|
||||
truncateLegends={truncateLegends}
|
||||
{...(stacked ? { stackBy: 'y' } : {})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ import { Location } from 'history';
|
|||
import React, { Component } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
|
@ -25,6 +26,7 @@ import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
|||
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
||||
import { LicenseContext } from '../../../../context/LicenseContext';
|
||||
import { TransactionLineChart } from './TransactionLineChart';
|
||||
import { isValidCoordinateValue } from '../../../../utils/isValidCoordinateValue';
|
||||
|
||||
interface TransactionChartProps {
|
||||
hasMLJob: boolean;
|
||||
|
@ -45,34 +47,25 @@ const ShiftedEuiText = styled(EuiText)`
|
|||
top: 5px;
|
||||
`;
|
||||
|
||||
const msTimeUnitLabel = i18n.translate(
|
||||
'xpack.apm.metrics.transactionChart.msTimeUnitLabel',
|
||||
{
|
||||
defaultMessage: 'ms'
|
||||
}
|
||||
);
|
||||
|
||||
export class TransactionCharts extends Component<TransactionChartProps> {
|
||||
public getResponseTimeTickFormatter = (t: number | null) => {
|
||||
return this.props.charts.noHits ? `- ${msTimeUnitLabel}` : asMillis(t);
|
||||
public getResponseTimeTickFormatter = (t: number) => {
|
||||
return asMillis(t);
|
||||
};
|
||||
|
||||
public getResponseTimeTooltipFormatter = (p: Coordinate) => {
|
||||
return this.props.charts.noHits || !p
|
||||
? `- ${msTimeUnitLabel}`
|
||||
: asMillis(p.y);
|
||||
return isValidCoordinateValue(p.y) ? asMillis(p.y) : NOT_AVAILABLE_LABEL;
|
||||
};
|
||||
|
||||
public getTPMFormatter = (t: number | null) => {
|
||||
const { urlParams, charts } = this.props;
|
||||
public getTPMFormatter = (t: number) => {
|
||||
const { urlParams } = this.props;
|
||||
const unit = tpmUnit(urlParams.transactionType);
|
||||
return charts.noHits || t === null
|
||||
? `- ${unit}`
|
||||
: `${asInteger(t)} ${unit}`;
|
||||
return `${asInteger(t)} ${unit}`;
|
||||
};
|
||||
|
||||
public getTPMTooltipFormatter = (p: Coordinate) => {
|
||||
return this.getTPMFormatter(p.y);
|
||||
return isValidCoordinateValue(p.y)
|
||||
? this.getTPMFormatter(p.y)
|
||||
: NOT_AVAILABLE_LABEL;
|
||||
};
|
||||
|
||||
public renderMLHeader(hasValidMlLicense: boolean) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { toQuery, fromQuery } from '../components/shared/Links/url_helpers';
|
||||
import { history } from '../utils/history';
|
||||
|
||||
const ChartsSyncContext = React.createContext<{
|
||||
hoverX: number | null;
|
||||
onHover: (hoverX: number) => void;
|
||||
onMouseLeave: () => void;
|
||||
onSelectionEnd: (range: { start: number; end: number }) => void;
|
||||
} | null>(null);
|
||||
|
||||
const ChartsSyncContextProvider: React.FC = ({ children }) => {
|
||||
const [time, setTime] = useState<number | null>(null);
|
||||
|
||||
const value = useMemo(() => {
|
||||
const hoverXHandlers = {
|
||||
onHover: (hoverX: number) => {
|
||||
setTime(hoverX);
|
||||
},
|
||||
onMouseLeave: () => {
|
||||
setTime(null);
|
||||
},
|
||||
onSelectionEnd: (range: { start: number; end: number }) => {
|
||||
setTime(null);
|
||||
|
||||
const currentSearch = toQuery(history.location.search);
|
||||
const nextSearch = {
|
||||
rangeFrom: new Date(range.start).toISOString(),
|
||||
rangeTo: new Date(range.end).toISOString()
|
||||
};
|
||||
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...currentSearch,
|
||||
...nextSearch
|
||||
})
|
||||
});
|
||||
},
|
||||
hoverX: time
|
||||
};
|
||||
|
||||
return { ...hoverXHandlers };
|
||||
}, [time, setTime]);
|
||||
|
||||
return <ChartsSyncContext.Provider value={value} children={children} />;
|
||||
};
|
||||
|
||||
export { ChartsSyncContext, ChartsSyncContextProvider };
|
|
@ -1,27 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
const ChartsTimeContext = React.createContext<{
|
||||
time: number | null;
|
||||
setTime: (time: number | null) => unknown;
|
||||
} | null>(null);
|
||||
|
||||
const ChartsTimeContextProvider: React.FC = ({ children }) => {
|
||||
const [time, setTime] = useState<number | null>(null);
|
||||
|
||||
const value = useMemo(() => {
|
||||
return {
|
||||
time,
|
||||
setTime
|
||||
};
|
||||
}, [time, setTime]);
|
||||
|
||||
return <ChartsTimeContext.Provider value={value} children={children} />;
|
||||
};
|
||||
|
||||
export { ChartsTimeContext, ChartsTimeContextProvider };
|
|
@ -5,13 +5,13 @@
|
|||
*/
|
||||
|
||||
import { useContext } from 'react';
|
||||
import { ChartsTimeContext } from '../context/ChartsTimeContext';
|
||||
import { ChartsSyncContext } from '../context/ChartsSyncContext';
|
||||
|
||||
export function useChartsTime() {
|
||||
const context = useContext(ChartsTimeContext);
|
||||
export function useChartsSync() {
|
||||
const context = useContext(ChartsSyncContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Missing ChartsTime context provider');
|
||||
throw new Error('Missing ChartsSync context provider');
|
||||
}
|
||||
|
||||
return context;
|
|
@ -6,10 +6,8 @@
|
|||
|
||||
import { useRef } from 'react';
|
||||
import { useFetcher } from './useFetcher';
|
||||
import { callApi } from '../services/rest/callApi';
|
||||
import { getUiFiltersES } from '../services/ui_filters/get_ui_filters_es';
|
||||
import { TransactionBreakdownAPIResponse } from '../../server/lib/transactions/breakdown';
|
||||
import { useUrlParams } from './useUrlParams';
|
||||
import { loadTransactionBreakdown } from '../services/rest/apm/transaction_groups';
|
||||
|
||||
export function useTransactionBreakdown() {
|
||||
const {
|
||||
|
@ -17,16 +15,18 @@ export function useTransactionBreakdown() {
|
|||
uiFilters
|
||||
} = useUrlParams();
|
||||
|
||||
const { data, error, status } = useFetcher(async () => {
|
||||
const {
|
||||
data = { kpis: [], timeseries: [] },
|
||||
error,
|
||||
status
|
||||
} = useFetcher(() => {
|
||||
if (serviceName && start && end) {
|
||||
return callApi<TransactionBreakdownAPIResponse>({
|
||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/breakdown`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
uiFiltersES: await getUiFiltersES(uiFilters)
|
||||
}
|
||||
return loadTransactionBreakdown({
|
||||
start,
|
||||
end,
|
||||
serviceName,
|
||||
transactionName,
|
||||
uiFilters
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, uiFilters]);
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
import { useMemo } from 'react';
|
||||
import { loadTransactionCharts } from '../services/rest/apm/transaction_groups';
|
||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useUiFilters } from '../context/UrlParamsContext';
|
||||
import { useFetcher } from './useFetcher';
|
||||
import { useUrlParams } from './useUrlParams';
|
||||
|
||||
export function useTransactionOverviewCharts(urlParams: IUrlParams) {
|
||||
const { serviceName, start, end, transactionType } = urlParams;
|
||||
const uiFilters = useUiFilters(urlParams);
|
||||
export function useTransactionCharts() {
|
||||
const {
|
||||
urlParams: { serviceName, transactionType, start, end, transactionName },
|
||||
uiFilters
|
||||
} = useUrlParams();
|
||||
|
||||
const { data, error, status } = useFetcher(() => {
|
||||
if (serviceName && start && end) {
|
||||
|
@ -21,15 +22,17 @@ export function useTransactionOverviewCharts(urlParams: IUrlParams) {
|
|||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
transactionType,
|
||||
uiFilters
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, transactionType, uiFilters]);
|
||||
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
|
||||
|
||||
const memoizedData = useMemo(() => getTransactionCharts(urlParams, data), [
|
||||
data
|
||||
]);
|
||||
const memoizedData = useMemo(
|
||||
() => getTransactionCharts({ transactionType }, data),
|
||||
[data]
|
||||
);
|
||||
|
||||
return {
|
||||
data: memoizedData,
|
|
@ -1,46 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { loadTransactionCharts } from '../services/rest/apm/transaction_groups';
|
||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useUiFilters } from '../context/UrlParamsContext';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
export function useTransactionDetailsCharts(urlParams: IUrlParams) {
|
||||
const {
|
||||
serviceName,
|
||||
transactionType,
|
||||
start,
|
||||
end,
|
||||
transactionName
|
||||
} = urlParams;
|
||||
const uiFilters = useUiFilters(urlParams);
|
||||
|
||||
const { data, error, status } = useFetcher(() => {
|
||||
if (serviceName && start && end && transactionName && transactionType) {
|
||||
return loadTransactionCharts({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
transactionType,
|
||||
uiFilters
|
||||
});
|
||||
}
|
||||
}, [serviceName, start, end, transactionName, transactionType, uiFilters]);
|
||||
|
||||
const memoizedData = useMemo(() => getTransactionCharts(urlParams, data), [
|
||||
data
|
||||
]);
|
||||
|
||||
return {
|
||||
data: memoizedData,
|
||||
status,
|
||||
error
|
||||
};
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ApmTimeSeriesResponse } from '../../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import {
|
||||
getAnomalyScoreSeries,
|
||||
getResponseTimeSeries,
|
||||
|
@ -34,8 +33,9 @@ describe('chartSelectors', () => {
|
|||
p95: [{ x: 0, y: 200 }, { x: 1000, y: 300 }],
|
||||
p99: [{ x: 0, y: 300 }, { x: 1000, y: 400 }]
|
||||
},
|
||||
tpmBuckets: [],
|
||||
overallAvgDuration: 200
|
||||
} as ApmTimeSeriesResponse;
|
||||
};
|
||||
|
||||
it('should produce correct series', () => {
|
||||
expect(
|
||||
|
@ -74,13 +74,20 @@ describe('chartSelectors', () => {
|
|||
});
|
||||
|
||||
describe('getTpmSeries', () => {
|
||||
const apmTimeseries = ({
|
||||
const apmTimeseries = {
|
||||
responseTimes: {
|
||||
avg: [],
|
||||
p95: [],
|
||||
p99: []
|
||||
},
|
||||
tpmBuckets: [
|
||||
{ key: 'HTTP 2xx', dataPoints: [{ x: 0, y: 5 }, { x: 0, y: 2 }] },
|
||||
{ key: 'HTTP 4xx', dataPoints: [{ x: 0, y: 1 }] },
|
||||
{ key: 'HTTP 5xx', dataPoints: [{ x: 0, y: 0 }] }
|
||||
]
|
||||
} as any) as ApmTimeSeriesResponse;
|
||||
],
|
||||
overallAvgDuration: 200
|
||||
};
|
||||
|
||||
const transactionType = 'MyTransactionType';
|
||||
it('should produce correct series', () => {
|
||||
expect(getTpmSeries(apmTimeseries, transactionType)).toEqual([
|
||||
|
@ -108,4 +115,25 @@ describe('chartSelectors', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty getTpmSeries', () => {
|
||||
const apmTimeseries = {
|
||||
responseTimes: {
|
||||
avg: [{ x: 0, y: 1 }, { x: 100, y: 1 }],
|
||||
p95: [{ x: 0, y: 1 }, { x: 100, y: 1 }],
|
||||
p99: [{ x: 0, y: 1 }, { x: 100, y: 1 }]
|
||||
},
|
||||
tpmBuckets: [],
|
||||
overallAvgDuration: 200
|
||||
};
|
||||
|
||||
const transactionType = 'MyTransactionType';
|
||||
it('should produce an empty series', () => {
|
||||
const series = getTpmSeries(apmTimeseries, transactionType);
|
||||
|
||||
expect(series[0].data.length).toBe(11);
|
||||
expect(series[0].data[0].x).toBe(0);
|
||||
expect(series[0].data[10].x).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '../../typings/timeseries';
|
||||
import { asDecimal, asMillis, tpmUnit } from '../utils/formatters';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { getEmptySeries } from '../components/shared/charts/CustomPlot/getEmptySeries';
|
||||
|
||||
export interface ITpmBucket {
|
||||
title: string;
|
||||
|
@ -29,14 +30,12 @@ export interface ITpmBucket {
|
|||
}
|
||||
|
||||
export interface ITransactionChartData {
|
||||
noHits: boolean;
|
||||
tpmSeries: ITpmBucket[];
|
||||
responseTimeSeries: TimeSeries[];
|
||||
}
|
||||
|
||||
const INITIAL_DATA = {
|
||||
apmTimeseries: {
|
||||
totalHits: 0,
|
||||
responseTimes: {
|
||||
avg: [],
|
||||
p95: [],
|
||||
|
@ -52,7 +51,6 @@ export function getTransactionCharts(
|
|||
{ transactionType }: IUrlParams,
|
||||
{ apmTimeseries, anomalyTimeseries }: TimeSeriesAPIResponse = INITIAL_DATA
|
||||
): ITransactionChartData {
|
||||
const noHits = apmTimeseries.totalHits === 0;
|
||||
const tpmSeries = getTpmSeries(apmTimeseries, transactionType);
|
||||
|
||||
const responseTimeSeries = getResponseTimeSeries({
|
||||
|
@ -61,7 +59,6 @@ export function getTransactionCharts(
|
|||
});
|
||||
|
||||
return {
|
||||
noHits,
|
||||
tpmSeries,
|
||||
responseTimeSeries
|
||||
};
|
||||
|
@ -162,12 +159,20 @@ export function getTpmSeries(
|
|||
const bucketKeys = tpmBuckets.map(({ key }) => key);
|
||||
const getColor = getColorByKey(bucketKeys);
|
||||
|
||||
const { avg } = apmTimeseries.responseTimes;
|
||||
|
||||
if (!tpmBuckets.length && avg.length) {
|
||||
const start = avg[0].x;
|
||||
const end = avg[avg.length - 1].x;
|
||||
return getEmptySeries(start, end);
|
||||
}
|
||||
|
||||
return tpmBuckets.map(bucket => {
|
||||
const avg = mean(bucket.dataPoints.map(p => p.y));
|
||||
const average = mean(bucket.dataPoints.map(p => p.y));
|
||||
return {
|
||||
title: bucket.key,
|
||||
data: bucket.dataPoints,
|
||||
legendValue: `${asDecimal(avg)} ${tpmUnit(transactionType || '')}`,
|
||||
legendValue: `${asDecimal(average)} ${tpmUnit(transactionType || '')}`,
|
||||
type: 'linemark',
|
||||
color: getColor(bucket.key)
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { TransactionBreakdownAPIResponse } from '../../../../server/lib/transactions/breakdown';
|
||||
import { TimeSeriesAPIResponse } from '../../../../server/lib/transactions/charts';
|
||||
import { ITransactionDistributionAPIResponse } from '../../../../server/lib/transactions/distribution';
|
||||
import { TransactionListAPIResponse } from '../../../../server/lib/transactions/get_top_transactions';
|
||||
|
@ -94,3 +95,27 @@ export async function loadTransactionCharts({
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadTransactionBreakdown({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
uiFilters
|
||||
}: {
|
||||
serviceName: string;
|
||||
start: string;
|
||||
end: string;
|
||||
transactionName?: string;
|
||||
uiFilters: UIFilters;
|
||||
}) {
|
||||
return callApi<TransactionBreakdownAPIResponse>({
|
||||
pathname: `/api/apm/services/${serviceName}/transaction_groups/breakdown`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
transactionName,
|
||||
uiFiltersES: await getUiFiltersES(uiFilters)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -172,15 +172,15 @@ function asTerabytes(value: number | null) {
|
|||
return `${asDecimal(value / 1e12)} TB`;
|
||||
}
|
||||
|
||||
export function asBytes(value: number | null) {
|
||||
if (value === null || isNaN(value)) {
|
||||
export function asBytes(value: number | null | undefined) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '';
|
||||
}
|
||||
return `${asDecimal(value)} B`;
|
||||
}
|
||||
|
||||
export function asDynamicBytes(value: number | null) {
|
||||
if (value === null || isNaN(value)) {
|
||||
export function asDynamicBytes(value: number | null | undefined) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '';
|
||||
}
|
||||
return unmemoizedFixedByteFormatter(value)(value);
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { flatten } from 'lodash';
|
||||
import { TimeSeries } from '../../typings/timeseries';
|
||||
|
||||
export const getRangeFromTimeSeries = (timeseries: TimeSeries[]) => {
|
||||
const dataPoints = flatten(timeseries.map(series => series.data));
|
||||
|
||||
if (dataPoints.length) {
|
||||
return {
|
||||
start: dataPoints[0].x,
|
||||
end: dataPoints[dataPoints.length - 1].x
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export const isValidCoordinateValue = (
|
||||
value: number | null | undefined
|
||||
): value is number => value !== null && value !== undefined;
|
|
@ -3,5 +3,19 @@
|
|||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
|
||||
export const MAX_KPIS = 20;
|
||||
|
||||
export const COLORS = [
|
||||
theme.euiColorVis0,
|
||||
theme.euiColorVis1,
|
||||
theme.euiColorVis2,
|
||||
theme.euiColorVis3,
|
||||
theme.euiColorVis4,
|
||||
theme.euiColorVis5,
|
||||
theme.euiColorVis6,
|
||||
theme.euiColorVis7,
|
||||
theme.euiColorVis8,
|
||||
theme.euiColorVis9
|
||||
];
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('getTransactionBreakdown', () => {
|
|||
|
||||
expect(response.kpis.length).toBe(0);
|
||||
|
||||
expect(Object.keys(response.timeseries_per_subtype).length).toBe(0);
|
||||
expect(Object.keys(response.timeseries).length).toBe(0);
|
||||
});
|
||||
|
||||
it('returns transaction breakdowns grouped by type and subtype', async () => {
|
||||
|
@ -53,18 +53,20 @@ describe('getTransactionBreakdown', () => {
|
|||
|
||||
expect(response.kpis.map(kpi => kpi.name)).toEqual([
|
||||
'app',
|
||||
'dispatcher-servlet',
|
||||
'http',
|
||||
'postgresql',
|
||||
'dispatcher-servlet'
|
||||
'postgresql'
|
||||
]);
|
||||
|
||||
expect(response.kpis[0]).toEqual({
|
||||
name: 'app',
|
||||
color: '#00b3a4',
|
||||
percentage: 0.5408550899466306
|
||||
});
|
||||
|
||||
expect(response.kpis[2]).toEqual({
|
||||
expect(response.kpis[3]).toEqual({
|
||||
name: 'postgresql',
|
||||
color: '#490092',
|
||||
percentage: 0.047366859295002
|
||||
});
|
||||
});
|
||||
|
@ -86,17 +88,22 @@ describe('getTransactionBreakdown', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const { timeseries_per_subtype: timeseriesPerSubtype } = response;
|
||||
const { timeseries } = response;
|
||||
|
||||
expect(Object.keys(timeseriesPerSubtype).length).toBe(4);
|
||||
expect(timeseries.length).toBe(4);
|
||||
|
||||
const appTimeseries = timeseries[0];
|
||||
expect(appTimeseries.title).toBe('app');
|
||||
expect(appTimeseries.type).toBe('areaStacked');
|
||||
expect(appTimeseries.hideLegend).toBe(true);
|
||||
|
||||
// empty buckets should result in null values for visible types
|
||||
expect(timeseriesPerSubtype.app.length).toBe(276);
|
||||
expect(timeseriesPerSubtype.app.length).not.toBe(257);
|
||||
expect(appTimeseries.data.length).toBe(276);
|
||||
expect(appTimeseries.data.length).not.toBe(257);
|
||||
|
||||
expect(timeseriesPerSubtype.app[0].x).toBe(1561102380000);
|
||||
expect(appTimeseries.data[0].x).toBe(1561102380000);
|
||||
|
||||
expect(timeseriesPerSubtype.app[0].y).toBeCloseTo(0.8689440187037277);
|
||||
expect(appTimeseries.data[0].y).toBeCloseTo(0.8689440187037277);
|
||||
});
|
||||
|
||||
it('should not include more KPIs than MAX_KPIs', async () => {
|
||||
|
@ -119,8 +126,8 @@ describe('getTransactionBreakdown', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const { timeseries_per_subtype: timeseriesPerSubtype } = response;
|
||||
const { timeseries } = response;
|
||||
|
||||
expect(Object.keys(timeseriesPerSubtype)).toEqual(['app', 'http']);
|
||||
expect(timeseries.map(serie => serie.title)).toEqual(['app', 'http']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,16 +18,12 @@ import { PromiseReturnType } from '../../../../typings/common';
|
|||
import { Setup } from '../../helpers/setup_request';
|
||||
import { rangeFilter } from '../../helpers/range_filter';
|
||||
import { getMetricsDateHistogramParams } from '../../helpers/metrics';
|
||||
import { MAX_KPIS } from './constants';
|
||||
import { MAX_KPIS, COLORS } from './constants';
|
||||
|
||||
export type TransactionBreakdownAPIResponse = PromiseReturnType<
|
||||
typeof getTransactionBreakdown
|
||||
>;
|
||||
|
||||
interface TimeseriesMap {
|
||||
[key: string]: Array<{ x: number; y: number | null }>;
|
||||
}
|
||||
|
||||
export async function getTransactionBreakdown({
|
||||
setup,
|
||||
serviceName,
|
||||
|
@ -154,12 +150,19 @@ export async function getTransactionBreakdown({
|
|||
return breakdowns;
|
||||
};
|
||||
|
||||
const kpis = sortByOrder(
|
||||
const visibleKpis = sortByOrder(
|
||||
formatBucket(resp.aggregations),
|
||||
'percentage',
|
||||
'desc'
|
||||
).slice(0, MAX_KPIS);
|
||||
|
||||
const kpis = sortByOrder(visibleKpis, 'name').map((kpi, index) => {
|
||||
return {
|
||||
...kpi,
|
||||
color: COLORS[index % COLORS.length]
|
||||
};
|
||||
});
|
||||
|
||||
const kpiNames = kpis.map(kpi => kpi.name);
|
||||
|
||||
const timeseriesPerSubtype = resp.aggregations.by_date.buckets.reduce(
|
||||
|
@ -168,26 +171,38 @@ export async function getTransactionBreakdown({
|
|||
const time = bucket.key;
|
||||
|
||||
return kpiNames.reduce((p, kpiName) => {
|
||||
const value = formattedValues.find(val => val.name === kpiName) || {
|
||||
const { name, percentage } = formattedValues.find(
|
||||
val => val.name === kpiName
|
||||
) || {
|
||||
name: kpiName,
|
||||
percentage: null
|
||||
};
|
||||
|
||||
const { name, percentage } = value;
|
||||
if (!p[name]) {
|
||||
p[name] = [];
|
||||
}
|
||||
return {
|
||||
...p,
|
||||
[value.name]: p[name].concat({ x: time, y: percentage })
|
||||
[name]: p[name].concat({
|
||||
x: time,
|
||||
y: percentage
|
||||
})
|
||||
};
|
||||
}, prev);
|
||||
},
|
||||
{} as TimeseriesMap
|
||||
{} as Record<string, Array<{ x: number; y: number | null }>>
|
||||
);
|
||||
|
||||
const timeseries = kpis.map(kpi => ({
|
||||
title: kpi.name,
|
||||
color: kpi.color,
|
||||
type: 'areaStacked',
|
||||
data: timeseriesPerSubtype[kpi.name],
|
||||
hideLegend: true
|
||||
}));
|
||||
|
||||
return {
|
||||
kpis,
|
||||
timeseries_per_subtype: timeseriesPerSubtype
|
||||
timeseries
|
||||
};
|
||||
}
|
||||
|
|
|
@ -983,7 +983,6 @@ Object {
|
|||
},
|
||||
],
|
||||
},
|
||||
"totalHits": 1297673,
|
||||
"tpmBuckets": Array [
|
||||
Object {
|
||||
"dataPoints": Array [
|
||||
|
|
|
@ -30,7 +30,6 @@ export function timeseriesTransformer({
|
|||
const tpmBuckets = getTpmBuckets(transactionResultBuckets, bucketSize);
|
||||
|
||||
return {
|
||||
totalHits: timeseriesResponse.hits.total,
|
||||
responseTimes: {
|
||||
avg,
|
||||
p95,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
export interface Coordinate {
|
||||
x: number;
|
||||
y: number | null;
|
||||
y: number | null | undefined;
|
||||
}
|
||||
|
||||
export interface RectCoordinate {
|
||||
|
|
|
@ -3856,7 +3856,6 @@
|
|||
"xpack.apm.metrics.plot.noDataLabel": "この時間範囲のデータがありません。",
|
||||
"xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:",
|
||||
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "平均期間の周りのストリームには予測バウンドが表示されます。異常スコアが >= 75 の場合、注釈が表示されます。",
|
||||
"xpack.apm.metrics.transactionChart.msTimeUnitLabel": "ms",
|
||||
"xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "ページ読み込み時間",
|
||||
"xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "1 分あたりのリクエスト",
|
||||
"xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "ルート変更時間",
|
||||
|
@ -10918,4 +10917,4 @@
|
|||
"xpack.watcher.watchActions.logging.logTextIsRequiredValidationMessage": "ログテキストが必要です。",
|
||||
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3857,7 +3857,6 @@
|
|||
"xpack.apm.metrics.plot.noDataLabel": "此时间范围内没有数据。",
|
||||
"xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning",
|
||||
"xpack.apm.metrics.transactionChart.machineLearningTooltip": "环绕平均持续时间的流显示预期边界。对 >= 75 的异常分数显示标注。",
|
||||
"xpack.apm.metrics.transactionChart.msTimeUnitLabel": "ms",
|
||||
"xpack.apm.metrics.transactionChart.pageLoadTimesLabel": "页面加载时间",
|
||||
"xpack.apm.metrics.transactionChart.requestsPerMinuteLabel": "每分钟请求数",
|
||||
"xpack.apm.metrics.transactionChart.routeChangeTimesLabel": "路由更改时间",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue