|
3 | 3 | * or more contributor license agreements. Licensed under the Elastic License;
|
4 | 4 | * you may not use this file except in compliance with the Elastic License.
|
5 | 5 | */
|
6 |
| -import { EuiIcon, EuiPageContentBody, EuiTitle } from '@elastic/eui'; |
7 |
| -import { |
8 |
| - EuiAreaSeries, |
9 |
| - EuiBarSeries, |
10 |
| - EuiCrosshairX, |
11 |
| - EuiDataPoint, |
12 |
| - EuiLineSeries, |
13 |
| - EuiSeriesChart, |
14 |
| - EuiSeriesChartProps, |
15 |
| - EuiSeriesProps, |
16 |
| - EuiXAxis, |
17 |
| - EuiYAxis, |
18 |
| -} from '@elastic/eui/lib/experimental'; |
| 6 | +import React, { useCallback } from 'react'; |
19 | 7 | import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
20 |
| -import Color from 'color'; |
21 | 8 | import { get } from 'lodash';
|
22 |
| -import moment from 'moment'; |
23 |
| -import React, { ReactText } from 'react'; |
24 |
| -import { InfraDataSeries, InfraMetricData, InfraTimerangeInput } from '../../../graphql/types'; |
25 |
| -import { InfraFormatter, InfraFormatterType } from '../../../lib/lib'; |
| 9 | +import { Axis, Chart, getAxisId, niceTimeFormatter, Position, Settings } from '@elastic/charts'; |
| 10 | +import { EuiPageContentBody, EuiTitle } from '@elastic/eui'; |
| 11 | +import { InfraMetricLayoutSection } from '../../../pages/metrics/layouts/types'; |
| 12 | +import { InfraMetricData, InfraTimerangeInput } from '../../../graphql/types'; |
| 13 | +import { getChartTheme } from '../../metrics_explorer/helpers/get_chart_theme'; |
| 14 | +import { InfraFormatterType } from '../../../lib/lib'; |
| 15 | +import { SeriesChart } from './series_chart'; |
26 | 16 | import {
|
27 |
| - InfraMetricLayoutSection, |
28 |
| - InfraMetricLayoutVisualizationType, |
29 |
| -} from '../../../pages/metrics/layouts/types'; |
30 |
| -import { createFormatter } from '../../../utils/formatters'; |
31 |
| - |
32 |
| -const MARGIN_LEFT = 60; |
33 |
| - |
34 |
| -const chartComponentsByType = { |
35 |
| - [InfraMetricLayoutVisualizationType.line]: EuiLineSeries, |
36 |
| - [InfraMetricLayoutVisualizationType.area]: EuiAreaSeries, |
37 |
| - [InfraMetricLayoutVisualizationType.bar]: EuiBarSeries, |
38 |
| -}; |
| 17 | + getFormatter, |
| 18 | + getMaxMinTimestamp, |
| 19 | + getChartName, |
| 20 | + getChartColor, |
| 21 | + getChartType, |
| 22 | + seriesHasLessThen2DataPoints, |
| 23 | +} from './helpers'; |
| 24 | +import { ErrorMessage } from './error_message'; |
39 | 25 |
|
40 | 26 | interface Props {
|
41 | 27 | section: InfraMetricLayoutSection;
|
42 | 28 | metric: InfraMetricData;
|
43 | 29 | onChangeRangeTime?: (time: InfraTimerangeInput) => void;
|
44 |
| - crosshairValue?: number; |
45 |
| - onCrosshairUpdate?: (crosshairValue: number) => void; |
46 | 30 | isLiveStreaming?: boolean;
|
47 | 31 | stopLiveStreaming?: () => void;
|
48 | 32 | intl: InjectedIntl;
|
49 | 33 | }
|
50 | 34 |
|
51 |
| -const isInfraMetricLayoutVisualizationType = ( |
52 |
| - subject: any |
53 |
| -): subject is InfraMetricLayoutVisualizationType => { |
54 |
| - return InfraMetricLayoutVisualizationType[subject] != null; |
55 |
| -}; |
56 |
| - |
57 |
| -const getChartName = (section: InfraMetricLayoutSection, seriesId: string) => { |
58 |
| - return get(section, ['visConfig', 'seriesOverrides', seriesId, 'name'], seriesId); |
59 |
| -}; |
60 |
| - |
61 |
| -const getChartColor = (section: InfraMetricLayoutSection, seriesId: string): string | undefined => { |
62 |
| - const color = new Color( |
63 |
| - get(section, ['visConfig', 'seriesOverrides', seriesId, 'color'], '#999') |
64 |
| - ); |
65 |
| - return color.hex().toString(); |
66 |
| -}; |
67 |
| - |
68 |
| -const getChartType = (section: InfraMetricLayoutSection, seriesId: string) => { |
69 |
| - const value = get(section, ['visConfig', 'type']); |
70 |
| - const overrideValue = get(section, ['visConfig', 'seriesOverrides', seriesId, 'type']); |
71 |
| - if (isInfraMetricLayoutVisualizationType(overrideValue)) { |
72 |
| - return overrideValue; |
73 |
| - } |
74 |
| - if (isInfraMetricLayoutVisualizationType(value)) { |
75 |
| - return value; |
76 |
| - } |
77 |
| - return InfraMetricLayoutVisualizationType.line; |
78 |
| -}; |
79 |
| - |
80 |
| -const getFormatter = (formatter: InfraFormatterType, formatterTemplate: string) => ( |
81 |
| - val: ReactText |
82 |
| -) => { |
83 |
| - if (val == null) { |
84 |
| - return ''; |
85 |
| - } |
86 |
| - return createFormatter(formatter, formatterTemplate)(val); |
87 |
| -}; |
88 |
| - |
89 |
| -const titleFormatter = (dataPoints: EuiDataPoint[]) => { |
90 |
| - if (dataPoints.length > 0) { |
91 |
| - const [firstDataPoint] = dataPoints; |
92 |
| - const { originalValues } = firstDataPoint; |
93 |
| - return { |
94 |
| - title: <EuiIcon type="clock" />, |
95 |
| - value: moment(originalValues.x).format('lll'), |
96 |
| - }; |
97 |
| - } |
98 |
| -}; |
99 |
| - |
100 |
| -const createItemsFormatter = ( |
101 |
| - formatter: InfraFormatter, |
102 |
| - labels: string[], |
103 |
| - seriesColors: string[] |
104 |
| -) => (dataPoints: EuiDataPoint[]) => { |
105 |
| - return dataPoints.map(d => { |
106 |
| - return { |
107 |
| - title: ( |
108 |
| - <span> |
109 |
| - <EuiIcon type="dot" style={{ color: seriesColors[d.seriesIndex] }} /> |
110 |
| - {labels[d.seriesIndex]} |
111 |
| - </span> |
112 |
| - ), |
113 |
| - value: formatter(d.y), |
114 |
| - }; |
115 |
| - }); |
116 |
| -}; |
117 |
| - |
118 |
| -const seriesHasLessThen2DataPoints = (series: InfraDataSeries): boolean => { |
119 |
| - return series.data.length < 2; |
120 |
| -}; |
121 |
| - |
122 | 35 | export const ChartSection = injectI18n(
|
123 |
| - class extends React.PureComponent<Props> { |
124 |
| - public static displayName = 'ChartSection'; |
125 |
| - public render() { |
126 |
| - const { crosshairValue, section, metric, onCrosshairUpdate, intl } = this.props; |
127 |
| - const { visConfig } = section; |
128 |
| - const crossHairProps = { |
129 |
| - crosshairValue, |
130 |
| - onCrosshairUpdate, |
131 |
| - }; |
132 |
| - const chartProps: EuiSeriesChartProps = { |
133 |
| - xType: 'time', |
134 |
| - showCrosshair: false, |
135 |
| - showDefaultAxis: false, |
136 |
| - enableSelectionBrush: true, |
137 |
| - onSelectionBrushEnd: this.handleSelectionBrushEnd, |
138 |
| - }; |
139 |
| - const stacked = visConfig && visConfig.stacked; |
140 |
| - if (stacked) { |
141 |
| - chartProps.stackBy = 'y'; |
142 |
| - } |
143 |
| - const bounds = visConfig && visConfig.bounds; |
144 |
| - if (bounds) { |
145 |
| - chartProps.yDomain = [bounds.min, bounds.max]; |
146 |
| - } |
147 |
| - if (!metric) { |
148 |
| - chartProps.statusText = intl.formatMessage({ |
149 |
| - id: 'xpack.infra.chartSection.missingMetricDataText', |
150 |
| - defaultMessage: 'Missing data', |
151 |
| - }); |
152 |
| - } |
153 |
| - if (metric.series.some(seriesHasLessThen2DataPoints)) { |
154 |
| - chartProps.statusText = intl.formatMessage({ |
155 |
| - id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderText', |
156 |
| - defaultMessage: 'Not enough data points to render chart, try increasing the time range.', |
157 |
| - }); |
158 |
| - } |
159 |
| - const formatter = get(visConfig, 'formatter', InfraFormatterType.number); |
160 |
| - const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}'); |
161 |
| - const formatterFunction = getFormatter(formatter, formatterTemplate); |
162 |
| - const seriesLabels = get(metric, 'series', [] as InfraDataSeries[]).map(s => |
163 |
| - getChartName(section, s.id) |
164 |
| - ); |
165 |
| - const seriesColors = get(metric, 'series', [] as InfraDataSeries[]).map( |
166 |
| - s => getChartColor(section, s.id) || '' |
| 36 | + ({ onChangeRangeTime, section, metric, intl, stopLiveStreaming, isLiveStreaming }: Props) => { |
| 37 | + const { visConfig } = section; |
| 38 | + const formatter = get(visConfig, 'formatter', InfraFormatterType.number); |
| 39 | + const formatterTemplate = get(visConfig, 'formatterTemplate', '{{value}}'); |
| 40 | + const valueFormatter = useCallback(getFormatter(formatter, formatterTemplate), [ |
| 41 | + formatter, |
| 42 | + formatterTemplate, |
| 43 | + ]); |
| 44 | + const dateFormatter = useCallback(niceTimeFormatter(getMaxMinTimestamp(metric)), [metric]); |
| 45 | + const handleTimeChange = useCallback( |
| 46 | + (from: number, to: number) => { |
| 47 | + if (onChangeRangeTime) { |
| 48 | + if (isLiveStreaming && stopLiveStreaming) { |
| 49 | + stopLiveStreaming(); |
| 50 | + } |
| 51 | + onChangeRangeTime({ |
| 52 | + from, |
| 53 | + to, |
| 54 | + interval: '>=1m', |
| 55 | + }); |
| 56 | + } |
| 57 | + }, |
| 58 | + [onChangeRangeTime, isLiveStreaming, stopLiveStreaming] |
| 59 | + ); |
| 60 | + |
| 61 | + if (!metric) { |
| 62 | + return ( |
| 63 | + <ErrorMessage |
| 64 | + title={intl.formatMessage({ |
| 65 | + id: 'xpack.infra.chartSection.missingMetricDataText', |
| 66 | + defaultMessage: 'Missing Data', |
| 67 | + })} |
| 68 | + body={intl.formatMessage({ |
| 69 | + id: 'xpack.infra.chartSection.missingMetricDataBody', |
| 70 | + defaultMessage: 'The data for this chart is missing.', |
| 71 | + })} |
| 72 | + /> |
167 | 73 | );
|
168 |
| - const itemsFormatter = createItemsFormatter(formatterFunction, seriesLabels, seriesColors); |
| 74 | + } |
| 75 | + |
| 76 | + if (metric.series.some(seriesHasLessThen2DataPoints)) { |
169 | 77 | return (
|
170 |
| - <EuiPageContentBody> |
171 |
| - <EuiTitle size="xs"> |
172 |
| - <h3 id={section.id}>{section.label}</h3> |
173 |
| - </EuiTitle> |
174 |
| - <div style={{ height: 200 }}> |
175 |
| - <EuiSeriesChart {...chartProps}> |
176 |
| - <EuiXAxis marginLeft={MARGIN_LEFT} /> |
177 |
| - <EuiYAxis tickFormat={formatterFunction} marginLeft={MARGIN_LEFT} /> |
178 |
| - <EuiCrosshairX |
179 |
| - marginLeft={MARGIN_LEFT} |
180 |
| - seriesNames={seriesLabels} |
181 |
| - itemsFormat={itemsFormatter} |
182 |
| - titleFormat={titleFormatter} |
183 |
| - {...crossHairProps} |
184 |
| - /> |
185 |
| - {metric && |
186 |
| - metric.series.map(series => { |
187 |
| - if (!series || series.data.length < 2) { |
188 |
| - return null; |
189 |
| - } |
190 |
| - const data = series.data.map(d => { |
191 |
| - return { x: d.timestamp, y: d.value || 0, y0: 0 }; |
192 |
| - }); |
193 |
| - const chartType = getChartType(section, series.id); |
194 |
| - const name = getChartName(section, series.id); |
195 |
| - const seriesProps: EuiSeriesProps = { |
196 |
| - data, |
197 |
| - name, |
198 |
| - lineSize: 2, |
199 |
| - }; |
200 |
| - const color = getChartColor(section, series.id); |
201 |
| - if (color) { |
202 |
| - seriesProps.color = color; |
203 |
| - } |
204 |
| - const EuiChartComponent = chartComponentsByType[chartType]; |
205 |
| - return ( |
206 |
| - <EuiChartComponent |
207 |
| - key={`${section.id}-${series.id}`} |
208 |
| - {...seriesProps} |
209 |
| - marginLeft={MARGIN_LEFT} |
210 |
| - /> |
211 |
| - ); |
212 |
| - })} |
213 |
| - </EuiSeriesChart> |
214 |
| - </div> |
215 |
| - </EuiPageContentBody> |
| 78 | + <ErrorMessage |
| 79 | + title={intl.formatMessage({ |
| 80 | + id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderTitle', |
| 81 | + defaultMessage: 'Not Enough Data', |
| 82 | + })} |
| 83 | + body={intl.formatMessage({ |
| 84 | + id: 'xpack.infra.chartSection.notEnoughDataPointsToRenderText', |
| 85 | + defaultMessage: |
| 86 | + 'Not enough data points to render chart, try increasing the time range.', |
| 87 | + })} |
| 88 | + /> |
216 | 89 | );
|
217 | 90 | }
|
218 | 91 |
|
219 |
| - private handleSelectionBrushEnd = (area: Area) => { |
220 |
| - const { onChangeRangeTime, isLiveStreaming, stopLiveStreaming } = this.props; |
221 |
| - const { startX, endX } = area.domainArea; |
222 |
| - if (onChangeRangeTime) { |
223 |
| - if (isLiveStreaming && stopLiveStreaming) { |
224 |
| - stopLiveStreaming(); |
225 |
| - } |
226 |
| - onChangeRangeTime({ |
227 |
| - to: endX.valueOf(), |
228 |
| - from: startX.valueOf(), |
229 |
| - interval: '>=1m', |
230 |
| - }); |
231 |
| - } |
232 |
| - }; |
| 92 | + return ( |
| 93 | + <EuiPageContentBody> |
| 94 | + <EuiTitle size="xs"> |
| 95 | + <h3 id={section.id}>{section.label}</h3> |
| 96 | + </EuiTitle> |
| 97 | + <div style={{ height: 200 }}> |
| 98 | + <Chart> |
| 99 | + <Axis |
| 100 | + id={getAxisId('timestamp')} |
| 101 | + position={Position.Bottom} |
| 102 | + showOverlappingTicks={true} |
| 103 | + tickFormat={dateFormatter} |
| 104 | + /> |
| 105 | + <Axis id={getAxisId('values')} position={Position.Left} tickFormat={valueFormatter} /> |
| 106 | + {metric && |
| 107 | + metric.series.map(series => ( |
| 108 | + <SeriesChart |
| 109 | + key={`series-${section.id}-${series.id}`} |
| 110 | + id={`series-${section.id}-${series.id}`} |
| 111 | + series={series} |
| 112 | + name={getChartName(section, series.id)} |
| 113 | + type={getChartType(section, series.id)} |
| 114 | + color={getChartColor(section, series.id)} |
| 115 | + stack={visConfig.stacked} |
| 116 | + /> |
| 117 | + ))} |
| 118 | + <Settings onBrushEnd={handleTimeChange} theme={getChartTheme()} /> |
| 119 | + </Chart> |
| 120 | + </div> |
| 121 | + </EuiPageContentBody> |
| 122 | + ); |
233 | 123 | }
|
234 | 124 | );
|
235 |
| - |
236 |
| -interface DomainArea { |
237 |
| - startX: moment.Moment; |
238 |
| - endX: moment.Moment; |
239 |
| - startY: number; |
240 |
| - endY: number; |
241 |
| -} |
242 |
| - |
243 |
| -interface DrawArea { |
244 |
| - x0: number; |
245 |
| - x1: number; |
246 |
| - y0: number; |
247 |
| - y1: number; |
248 |
| -} |
249 |
| - |
250 |
| -interface Area { |
251 |
| - domainArea: DomainArea; |
252 |
| - drawArea: DrawArea; |
253 |
| -} |
0 commit comments