Skip to content

Commit 4e9d981

Browse files
Fix TSVB table trend slope value (#71087)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent 5ccc7e9 commit 4e9d981

File tree

4 files changed

+170
-10
lines changed

4 files changed

+170
-10
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@
256256
"redux-actions": "^2.6.5",
257257
"redux-thunk": "^2.3.0",
258258
"regenerator-runtime": "^0.13.3",
259-
"regression": "2.0.1",
260259
"request": "^2.88.0",
261260
"require-in-the-middle": "^5.0.2",
262261
"reselect": "^4.0.0",

src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,20 @@
2020
import { buildProcessorFunction } from '../build_processor_function';
2121
import { processors } from '../response_processors/table';
2222
import { getLastValue } from '../../../../common/get_last_value';
23-
import regression from 'regression';
2423
import { first, get } from 'lodash';
2524
import { overwrite } from '../helpers';
2625
import { getActiveSeries } from '../helpers/get_active_series';
2726

27+
function trendSinceLastBucket(data) {
28+
if (data.length < 2) {
29+
return 0;
30+
}
31+
const currentBucket = data[data.length - 1];
32+
const prevBucket = data[data.length - 2];
33+
const trend = (currentBucket[1] - prevBucket[1]) / currentBucket[1];
34+
return Number.isNaN(trend) ? 0 : trend;
35+
}
36+
2837
export function processBucket(panel) {
2938
return (bucket) => {
3039
const series = getActiveSeries(panel).map((series) => {
@@ -38,14 +47,12 @@ export function processBucket(panel) {
3847
};
3948
overwrite(bucket, series.id, { meta, timeseries });
4049
}
41-
4250
const processor = buildProcessorFunction(processors, bucket, panel, series);
4351
const result = first(processor([]));
4452
if (!result) return null;
4553
const data = get(result, 'data', []);
46-
const linearRegression = regression.linear(data);
54+
result.slope = trendSinceLastBucket(data);
4755
result.last = getLastValue(data);
48-
result.slope = linearRegression.equation[0];
4956
return result;
5057
});
5158
return { key: bucket.key, series };
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import { processBucket } from './process_bucket';
21+
22+
function createValueObject(key, value, seriesId) {
23+
return { key_as_string: `${key}`, doc_count: value, key, [seriesId]: { value } };
24+
}
25+
26+
function createBucketsObjects(size, sort, seriesId) {
27+
const values = Array(size)
28+
.fill(1)
29+
.map((_, i) => i + 1);
30+
if (sort === 'flat') {
31+
return values.map((_, i) => createValueObject(i, 1, seriesId));
32+
}
33+
if (sort === 'desc') {
34+
return values.reverse().map((v, i) => createValueObject(i, v, seriesId));
35+
}
36+
return values.map((v, i) => createValueObject(i, v, seriesId));
37+
}
38+
39+
function createPanel(series) {
40+
return {
41+
type: 'table',
42+
time_field: '',
43+
series: series.map((seriesId) => ({
44+
id: seriesId,
45+
metrics: [{ id: seriesId, type: 'count' }],
46+
trend_arrows: 1,
47+
})),
48+
};
49+
}
50+
51+
function createBuckets(series) {
52+
return [
53+
{ key: 'A', trend: 'asc', size: 10 },
54+
{ key: 'B', trend: 'desc', size: 10 },
55+
{ key: 'C', trend: 'flat', size: 10 },
56+
{ key: 'D', trend: 'asc', size: 1, expectedTrend: 'flat' },
57+
].map(({ key, trend, size, expectedTrend }) => {
58+
const baseObj = {
59+
key,
60+
expectedTrend: expectedTrend || trend,
61+
};
62+
for (const seriesId of series) {
63+
baseObj[seriesId] = {
64+
meta: {
65+
timeField: 'timestamp',
66+
seriesId: seriesId,
67+
},
68+
buckets: createBucketsObjects(size, trend, seriesId),
69+
};
70+
}
71+
return baseObj;
72+
});
73+
}
74+
75+
function trendChecker(trend, slope) {
76+
switch (trend) {
77+
case 'asc':
78+
return slope > 0;
79+
case 'desc':
80+
return slope <= 0;
81+
case 'flat':
82+
return slope === 0;
83+
default:
84+
throw Error(`Slope value ${slope} not valid for trend "${trend}"`);
85+
}
86+
}
87+
88+
describe('processBucket(panel)', () => {
89+
describe('single metric panel', () => {
90+
let panel;
91+
const SERIES_ID = 'series-id';
92+
93+
beforeEach(() => {
94+
panel = createPanel([SERIES_ID]);
95+
});
96+
97+
test('return the correct trend direction', () => {
98+
const bucketProcessor = processBucket(panel);
99+
const buckets = createBuckets([SERIES_ID]);
100+
for (const bucket of buckets) {
101+
const result = bucketProcessor(bucket);
102+
expect(result.key).toEqual(bucket.key);
103+
expect(trendChecker(bucket.expectedTrend, result.series[0].slope)).toBeTruthy();
104+
}
105+
});
106+
107+
test('properly handle 0 values for trend', () => {
108+
const bucketProcessor = processBucket(panel);
109+
const bucketforNaNResult = {
110+
key: 'NaNScenario',
111+
expectedTrend: 'flat',
112+
[SERIES_ID]: {
113+
meta: {
114+
timeField: 'timestamp',
115+
seriesId: SERIES_ID,
116+
},
117+
buckets: [
118+
// this is a flat case, but 0/0 has not a valid number result
119+
createValueObject(0, 0, SERIES_ID),
120+
createValueObject(1, 0, SERIES_ID),
121+
],
122+
},
123+
};
124+
const result = bucketProcessor(bucketforNaNResult);
125+
expect(result.key).toEqual(bucketforNaNResult.key);
126+
expect(trendChecker(bucketforNaNResult.expectedTrend, result.series[0].slope)).toEqual(true);
127+
});
128+
129+
test('have the side effect to create the timeseries property if missing on bucket', () => {
130+
const bucketProcessor = processBucket(panel);
131+
const buckets = createBuckets([SERIES_ID]);
132+
for (const bucket of buckets) {
133+
bucketProcessor(bucket);
134+
expect(bucket[SERIES_ID].buckets).toBeUndefined();
135+
expect(bucket[SERIES_ID].timeseries).toBeDefined();
136+
}
137+
});
138+
});
139+
140+
describe('multiple metrics panel', () => {
141+
let panel;
142+
const SERIES = ['series-id-1', 'series-id-2'];
143+
144+
beforeEach(() => {
145+
panel = createPanel(SERIES);
146+
});
147+
148+
test('return the correct trend direction', () => {
149+
const bucketProcessor = processBucket(panel);
150+
const buckets = createBuckets(SERIES);
151+
for (const bucket of buckets) {
152+
const result = bucketProcessor(bucket);
153+
expect(result.key).toEqual(bucket.key);
154+
expect(trendChecker(bucket.expectedTrend, result.series[0].slope)).toBeTruthy();
155+
expect(trendChecker(bucket.expectedTrend, result.series[1].slope)).toBeTruthy();
156+
}
157+
});
158+
});
159+
});

yarn.lock

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26736,11 +26736,6 @@ regjsparser@^0.6.4:
2673626736
dependencies:
2673726737
jsesc "~0.5.0"
2673826738

26739-
regression@2.0.1:
26740-
version "2.0.1"
26741-
resolved "https://registry.yarnpkg.com/regression/-/regression-2.0.1.tgz#8d29c3e8224a10850c35e337e85a8b2fac3b0c87"
26742-
integrity sha1-jSnD6CJKEIUMNeM36FqLL6w7DIc=
26743-
2674426739
rehype-parse@^6.0.0:
2674526740
version "6.0.0"
2674626741
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-6.0.0.tgz#f681555f2598165bee2c778b39f9073d17b16bca"

0 commit comments

Comments
 (0)