diff --git a/.eslintrc.js b/.eslintrc.js index 08bc83d5b4..973bf18e97 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -52,7 +52,6 @@ module.exports = { }, ], 'sort-keys': 'off', - 'import/no-default-export': 'error', 'import/no-unresolved': 'error', 'no-irregular-whitespace': 'error', 'no-unused-expressions': 'error', @@ -78,7 +77,7 @@ module.exports = { }, overrides: [ { - files: ['*.js'], + files: ['*.js', '*test.ts'], rules: { '@typescript-eslint/no-var-requires': 0, }, diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index ea6b06dcbe..98c31be60b 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -1,38 +1,66 @@ import React from 'react'; -import { - Axis, - Chart, - getAxisId, - getSpecId, - Position, - ScaleType, - HistogramBarSeries, - DARK_THEME, - Settings, -} from '../src'; -import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; +import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, Settings, LineSeries } from '../src'; +import { Fit } from '../src/chart_types/xy_chart/utils/specs'; + +const data = [ + { x: 0, y: null }, + { x: 1, y: 3 }, + { x: 2, y: 5 }, + { x: 3, y: null }, + { x: 4, y: 4 }, + { x: 5, y: null }, + { x: 6, y: 5 }, + { x: 7, y: 6 }, + { x: 8, y: null }, + { x: 9, y: null }, + { x: 10, y: null }, + { x: 11, y: 12 }, + { x: 12, y: null }, +]; export class Playground extends React.Component { render() { - const data = KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 5); return ( -
- - - - - - - -
+ <> +
+ + + + + + +
+ ); } } diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000000..0529f84411 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,2 @@ +// https://github.com/jest-community/jest-extended +import 'jest-extended'; diff --git a/integration/helpers.ts b/integration/helpers.ts index 04f4b627bc..128326a1f4 100644 --- a/integration/helpers.ts +++ b/integration/helpers.ts @@ -24,6 +24,7 @@ function requireAllStories() { function encodeString(string: string) { return string .replace(/\//gi, ' ') + .replace(/-/g, ' ') .replace(/[^a-z|A-Z|0-9|\s|\/]+/gi, '') .trim() .replace(/\s+/g, '-') diff --git a/integration/page_objects/common.ts b/integration/page_objects/common.ts index 2178a2eba8..2a0bd73307 100644 --- a/integration/page_objects/common.ts +++ b/integration/page_objects/common.ts @@ -107,7 +107,7 @@ class CommonPage { expect(chart).toMatchImageSnapshot(); } catch (error) { - throw new Error(error); + throw new Error(`${error}\n\n${url}`); } } async loadChartFromURL(url: string) { diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..5ee342375a Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png new file mode 100644 index 0000000000..ec4a40e69e Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-area-chart-stacked-with-separated-specs-same-naming-visually-looks-correct-2-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..49f9ad800b Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-mixed-charts-fitting-functions-non-stacked-series-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png new file mode 100644 index 0000000000..874341b22b Binary files /dev/null and b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-linear-point-alignment-start-2-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..1073d00e16 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..8bbf926bec Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..03c0c7ae28 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..71ecc0c0f0 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..57ffc660a5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..2a17078745 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..ef63804ab2 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..df37f50c84 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..1f9b3b14ba Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..fe13aacb14 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..03c0c7ae28 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..179f089fc6 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..e502d2bf7d Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..2a17078745 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..2f24132a79 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..df37f50c84 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-end-value-set-to-nearest-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..49f9ad800b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..a23ceb9594 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..03c0c7ae28 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..f4eee894a6 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..3152e341f9 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..2a17078745 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..ef63804ab2 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..df37f50c84 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..99fdde5816 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..6433f597a7 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..f296c81817 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..587c4c4094 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..5f3be43a8c Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..4ae788a2f5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..4a4a07a18e Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..5520db6a6a Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-ordinal-dataset-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..1073d00e16 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..8bbf926bec Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..03c0c7ae28 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..71ecc0c0f0 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..57ffc660a5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..2a17078745 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..ef63804ab2 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..df37f50c84 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-area-charts-with-curved-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..ff328cfec9 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..0c41dfe5b1 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..40f3e66fe5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..d89b6e5aed Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..936c6fc8ee Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..d46d71eb5b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..a2f2eb5370 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..527a9f603b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..81f62480fb Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..4a49c5f6d8 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..40f3e66fe5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..78ba866112 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..0e142da1bc Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..1bc9eeb764 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..a2f2eb5370 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..527a9f603b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-no-end-value-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png new file mode 100644 index 0000000000..ff328cfec9 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-average-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png new file mode 100644 index 0000000000..0c41dfe5b1 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-carry-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png new file mode 100644 index 0000000000..40f3e66fe5 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-explicit-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png new file mode 100644 index 0000000000..d89b6e5aed Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-linear-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png new file mode 100644 index 0000000000..936c6fc8ee Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-lookahead-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png new file mode 100644 index 0000000000..d46d71eb5b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-nearest-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png new file mode 100644 index 0000000000..a2f2eb5370 Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-none-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png new file mode 100644 index 0000000000..527a9f603b Binary files /dev/null and b/integration/tests/__image_snapshots__/mixed-stories-test-ts-mixed-series-stories-fitting-functions-line-charts-with-curve-end-value-set-to-2-should-display-correct-fit-for-type-zero-1-snap.png differ diff --git a/integration/tests/all.test.ts b/integration/tests/all.test.ts index 46899d746e..a3dfb202ba 100644 --- a/integration/tests/all.test.ts +++ b/integration/tests/all.test.ts @@ -14,7 +14,8 @@ describe('Baseline Visual tests for all stories', () => { stories.forEach(({ title, encodedTitle }) => { describe(title, () => { it('visually looks correct', async () => { - await common.expectChartAtUrlToMatchScreenshot(`http://localhost:9001?id=${encodedGroup}--${encodedTitle}`); + const url = `http://localhost:9001?id=${encodedGroup}--${encodedTitle}`; + await common.expectChartAtUrlToMatchScreenshot(url); }); }); }); diff --git a/integration/tests/mixed-stories.test.ts b/integration/tests/mixed-stories.test.ts new file mode 100644 index 0000000000..855fa25c23 --- /dev/null +++ b/integration/tests/mixed-stories.test.ts @@ -0,0 +1,86 @@ +import { common } from '../page_objects'; +import { Fit } from '../../src/chart_types/xy_chart/utils/specs'; + +describe('Mixed series stories', () => { + describe('Fitting functions', () => { + describe('Area charts - no endValue', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Area charts - endValue set to 2', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Area charts - endValue set to "nearest"', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=nearest&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Area charts - with curved - endValue set to 2', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=1&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Area charts - Ordinal dataset - no endValue', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=area&knob-dataset=ordinal&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Line charts - no endValue', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=none&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Line charts - endValue set to 2', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=0&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + + describe('Line charts - with curve - endValue set to 2', () => { + Object.values(Fit).forEach((fitType) => { + it(`should display correct fit for type - ${fitType}`, async () => { + await common.expectChartAtUrlToMatchScreenshot( + `http://localhost:9001/?path=/story/mixed-charts--fitting-functions-non-stacked-series&knob-seriesType=line&knob-dataset=all&knob-fitting function=${fitType}&knob-Curve=1&knob-End value=2&knob-Explicit valuve (using Fit.Explicit)=8`, + ); + }); + }); + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index 49155c8540..726b4296e1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,7 +2,9 @@ module.exports = { roots: ['/src'], preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom-fourteen', - setupFilesAfterEnv: ['/scripts/setup_enzyme.ts'], + setupFilesAfterEnv: ['jest-extended', '/scripts/setup_enzyme.ts', '/scripts/custom_matchers.ts'], + coveragePathIgnorePatterns: ['/src/mocks/', '/node_modules/'], + clearMocks: true, globals: { 'ts-jest': { tsConfig: 'tsconfig.jest.json', diff --git a/package.json b/package.json index 39fad2c58a..0a8a989a56 100644 --- a/package.json +++ b/package.json @@ -25,19 +25,19 @@ "storybook:build": "rm -rf .out && build-storybook -c .storybook -o .out", "lint": "NODE_ENV=production eslint --ext .tsx,.ts,.js .", "lint:fix": "yarn lint --fix", + "test": "jest --verbose --config jest.config.js", "pq": "pretty-quick", - "test": "jest --config jest.config.js", "test:tz": "yarn test:tz-utc && yarn test:tz-ny && yarn test:tz-jp", - "test:tz-utc": "TZ=UTC jest --config=jest.tz.config.js", - "test:tz-ny": "TZ=America/New_York jest --config=jest.tz.config.js", - "test:tz-jp": "TZ=Asia/Tokyo jest --config=jest.tz.config.js", + "test:tz-utc": "TZ=UTC jest --verbose --config=jest.tz.config.js", + "test:tz-ny": "TZ=America/New_York jest --verbose --config=jest.tz.config.js", + "test:tz-jp": "TZ=Asia/Tokyo jest --verbose --config=jest.tz.config.js", "watch": "yarn test --watch", "semantic-release": "semantic-release", "typecheck:src": "yarn build:ts --noEmit", "typecheck:all": "tsc -p ./tsconfig.json --noEmit", "playground": "cd .playground && webpack-dev-server", "playground:ie": "cd .playground && webpack-dev-server --host=0.0.0.0 --disable-host-check --useLocalIp", - "jest:integration": "TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --rootDir=integration -c=integration/jest.config.js --runInBand" + "jest:integration": "TZ=UTC JEST_PUPPETEER_CONFIG=integration/jest-puppeteer.config.js jest --verbose --rootDir=integration -c=integration/jest.config.js --runInBand" }, "files": [ "dist/**/*", @@ -110,9 +110,12 @@ "husky": "^1.3.1", "jest": "^24.9.0", "jest-environment-jsdom-fourteen": "^0.1.0", + "jest-extended": "^0.11.2", "jest-image-snapshot": "^2.11.0", + "jest-matcher-utils": "^24.9.0", "jest-puppeteer": "^4.3.0", "jest-puppeteer-docker": "^1.2.0", + "lodash": "^4.17.15", "lorem-ipsum": "^2.0.3", "luxon": "^1.11.3", "moment-timezone": "^0.5.27", @@ -132,6 +135,7 @@ "ts-jest": "^24.1.0", "ts-loader": "^6.1.2", "typescript": "^3.6.3", + "utility-types": "^3.8.0", "webpack": "^4.29.5", "webpack-cli": "^3.3.1", "webpack-dev-server": "^3.3.1" diff --git a/scripts/custom_matchers.ts b/scripts/custom_matchers.ts new file mode 100644 index 0000000000..bf6d226a8e --- /dev/null +++ b/scripts/custom_matchers.ts @@ -0,0 +1,70 @@ +import { matcherErrorMessage } from 'jest-matcher-utils'; + +// ensure this is parsed as a module. +export {}; + +/** + * Final Matcher type with `this` and `received` args removed from jest matcher function + */ +type MatcherParameters any> = T extends ( + this: any, + received: any, + ...args: infer P +) => any + ? P + : never; + +declare global { + namespace jest { // eslint-disable-line + interface Matchers { + /** + * Expect array to be filled with value, and optionally length + */ + toEqualArrayOf(...args: MatcherParameters): R; + } + } +} + +/** + * Expect array to be filled with value, and optionally length + */ +function toEqualArrayOf(this: jest.MatcherUtils, received: any[], value: any, length?: number) { + const matcherName = 'toEqualArrayOf'; + + if (!Array.isArray(received)) { + throw new Error( + matcherErrorMessage( + this.utils.matcherHint(matcherName), + `${this.utils.RECEIVED_COLOR('received')} value must be an array.`, + `Received type: ${typeof received}`, + ), + ); + } + + const receivedPretty = this.utils.printReceived(received); + const elementCheck = received.every((v) => v === value); + const lengthCheck = length === undefined || received.length === length; + + if (!lengthCheck) { + return { + pass: false, + message: () => `expected array length to be ${length} but got ${received.length}`, + }; + } + + if (!elementCheck) { + return { + pass: false, + message: () => `expected ${receivedPretty} to be an array of ${value}'s`, + }; + } + + return { + pass: true, + message: () => `expected ${receivedPretty} not to be an array of ${value}'s`, + }; +} + +expect.extend({ + toEqualArrayOf, +}); diff --git a/src/chart_types/xy_chart/rendering/rendering.test.ts b/src/chart_types/xy_chart/rendering/rendering.test.ts index 4ae2eff003..45b0997b5e 100644 --- a/src/chart_types/xy_chart/rendering/rendering.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.test.ts @@ -7,10 +7,13 @@ import { getBarStyleOverrides, GeometryId, getPointStyleOverrides, + getClippedRanges, } from './rendering'; import { BarSeriesStyle, SharedGeometryStateStyle, PointStyle } from '../../../utils/themes/theme'; import { DataSeriesDatum } from '../utils/series'; import { RecursivePartial, mergePartial } from '../../../utils/commons'; +import { MockDataSeries } from '../../../mocks'; +import { MockScale } from '../../../mocks/scale'; describe('Rendering utils', () => { test('check if point is in geometry', () => { @@ -330,4 +333,56 @@ describe('Rendering utils', () => { expect(styleOverrides).toEqual(expectedStyles); }); }); + + describe('getClippedRanges', () => { + const dataSeries = MockDataSeries.fitFunction({ shuffle: false }); + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + bandwidth: 0, + range: [dataSeries.data[0].x as number, dataSeries.data[12].x as number], + }); + + it('should return array pairs of non-null x regions with null end values', () => { + const actual = getClippedRanges(dataSeries.data, xScale, 0); + + expect(actual).toEqual([[0, 1], [2, 4], [4, 6], [7, 11], [11, 12]]); + }); + + it('should return array pairs of non-null x regions with valid end values', () => { + const data = dataSeries.data.slice(1, -1); + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + range: [data[0].x as number, data[10].x as number], + }); + const actual = getClippedRanges(data, xScale, 0); + + expect(actual).toEqual([[2, 4], [4, 6], [7, 11]]); + }); + + it('should account for bandwidth', () => { + const bandwidth = 2; + const xScale = MockScale.default({ + scale: jest.fn().mockImplementation((x) => x), + bandwidth, + range: [dataSeries.data[0].x as number, (dataSeries.data[12].x as number) + bandwidth * (2 / 3)], + }); + const actual = getClippedRanges(dataSeries.data, xScale, 0); + + expect(actual).toEqual([[0, 2], [3, 5], [5, 7], [8, 12]]); + }); + + it('should account for xScaleOffset', () => { + const actual = getClippedRanges(dataSeries.data, xScale, 2); + + expect(actual).toEqual([[0, -1], [0, 2], [2, 4], [5, 9]]); + }); + + it('should call scale to get x value for each datum', () => { + getClippedRanges(dataSeries.data, xScale, 0); + + expect(xScale.scale).toHaveBeenNthCalledWith(1, dataSeries.data[0].x); + expect(xScale.scale).toHaveBeenCalledTimes(dataSeries.data.length); + expect(xScale.scale).toHaveBeenCalledWith(dataSeries.data[12].x); + }); + }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 03a3b6b7f0..7565f5b6c3 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -1,4 +1,5 @@ import { area, line } from 'd3-shape'; +import { $Values } from 'utility-types'; import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; import { @@ -34,7 +35,7 @@ export const AccessorType = Object.freeze({ Y1: 'y1' as 'y1', }); -export type AccessorType = typeof AccessorType.Y0 | typeof AccessorType.Y1; +export type AccessorType = $Values; export interface GeometryValue { y: any; @@ -44,6 +45,13 @@ export interface GeometryValue { export type IndexedGeometry = PointGeometry | BarGeometry; +/** + * Array of **range** clippings [x1, x2] to be excluded during rendering + * + * Note: Must be scaled **range** values (i.e. pixel coordinates) **NOT** domain values + */ +export type ClippedRanges = [number, number][]; + export interface PointGeometry { x: number; y: number; @@ -85,6 +93,10 @@ export interface LineGeometry { geometryId: GeometryId; seriesLineStyle: LineStyle; seriesPointStyle: PointStyle; + /** + * Ranges of `[x0, x1]` pairs to clip from series + */ + clippedRanges: ClippedRanges; } export interface AreaGeometry { area: string; @@ -100,6 +112,10 @@ export interface AreaGeometry { seriesAreaLineStyle: LineStyle; seriesPointStyle: PointStyle; isStacked: boolean; + /** + * Ranges of `[x0, x1]` pairs to clip from series + */ + clippedRanges: ClippedRanges; } export function isPointGeometry(ig: IndexedGeometry): ig is PointGeometry { @@ -407,6 +423,7 @@ export function renderLine( xScaleOffset: number, seriesStyle: LineSeriesStyle, pointStyleAccessor?: PointStyleAccessor, + hasFit?: boolean, ): { lineGeometry: LineGeometry; indexedGeometries: Map; @@ -415,15 +432,19 @@ export function renderLine( const pathGenerator = line() .x(({ x }) => xScale.scale(x) - xScaleOffset) - .y(({ y1 }) => { - if (y1 !== null) { - return yScale.scale(y1); + .y((datum) => { + const yValue = getYValue(datum); + + if (yValue !== null) { + return yScale.scale(yValue); } + // this should never happen thanks to the defined function return yScale.isInverted ? yScale.range[1] : yScale.range[0]; }) - .defined(({ x, y1 }) => { - return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x); + .defined((datum) => { + const yValue = getYValue(datum); + return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x); }) .curve(getCurveFactory(curve)); const y = 0; @@ -441,6 +462,7 @@ export function renderLine( pointStyleAccessor, ); + const clippedRanges = hasFit && !hasY0Accessors ? getClippedRanges(dataset, xScale, xScaleOffset) : []; const lineGeometry = { line: pathGenerator(dataset) || '', points: pointGeometries, @@ -455,6 +477,7 @@ export function renderLine( }, seriesLineStyle: seriesStyle.line, seriesPointStyle: seriesStyle.point, + clippedRanges, }; return { lineGeometry, @@ -462,6 +485,21 @@ export function renderLine( }; } +/** + * Returns value of `y1` or `filled.y1` or null + */ +export const getYValue = ({ y1, filled }: DataSeriesDatum): number | null => { + if (y1 !== null) { + return y1; + } + + if (filled && filled.y1 !== undefined) { + return filled.y1; + } + + return null; +}; + export function renderArea( shift: number, dataset: DataSeriesDatum[], @@ -476,17 +514,18 @@ export function renderArea( seriesStyle: AreaSeriesStyle, isStacked = false, pointStyleAccessor?: PointStyleAccessor, + hasFit?: boolean, ): { areaGeometry: AreaGeometry; indexedGeometries: Map; } { const isLogScale = isLogarithmicScale(yScale); - const pathGenerator = area() .x(({ x }) => xScale.scale(x) - xScaleOffset) - .y1(({ y1 }) => { - if (y1 !== null) { - return yScale.scale(y1); + .y1((datum) => { + const yValue = getYValue(datum); + if (yValue !== null) { + return yScale.scale(yValue); } // this should never happen thanks to the defined function return yScale.isInverted ? yScale.range[1] : yScale.range[0]; @@ -497,13 +536,14 @@ export function renderArea( } return yScale.scale(y0); }) - .defined(({ y1, x }) => { - return y1 !== null && !(isLogScale && y1 <= 0) && xScale.isValueInDomain(x); + .defined((datum) => { + const yValue = getYValue(datum); + return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x); }) .curve(getCurveFactory(curve)); + const clippedRanges = hasFit && !hasY0Accessors && !isStacked ? getClippedRanges(dataset, xScale, xScaleOffset) : []; const y1Line = pathGenerator.lineY1()(dataset); - const lines: string[] = []; if (y1Line) { lines.push(y1Line); @@ -527,7 +567,7 @@ export function renderArea( pointStyleAccessor, ); - const areaGeometry = { + const areaGeometry: AreaGeometry = { area: pathGenerator(dataset) || '', lines, points: pointGeometries, @@ -544,6 +584,7 @@ export function renderArea( seriesAreaLineStyle: seriesStyle.line, seriesPointStyle: seriesStyle.point, isStacked, + clippedRanges, }; return { areaGeometry, @@ -551,6 +592,41 @@ export function renderArea( }; } +/** + * Gets clipped ranges that have been fitted to values + * @param dataset + * @param xScale + * @param xScaleOffset + */ +export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xScaleOffset: number): ClippedRanges { + let firstNonNullX: number | null = null; + let hasNull = false; + + return dataset.reduce((acc, { x, y1 }) => { + const xValue = xScale.scale(x) - xScaleOffset + xScale.bandwidth / 2; + + if (y1 !== null) { + if (hasNull) { + if (firstNonNullX !== null) { + acc.push([firstNonNullX, xValue]); + } else { + acc.push([0, xValue]); + } + hasNull = false; + } + + firstNonNullX = xValue; + } else { + const endXValue = xScale.range[1] - xScale.bandwidth * (2 / 3); + if (firstNonNullX !== null && xValue === endXValue) { + acc.push([firstNonNullX, xValue]); + } + hasNull = true; + } + return acc; + }, []); +} + export function getGeometryStateStyle( geometryId: GeometryId, highlightedLegendItem: LegendItem | null, diff --git a/src/chart_types/xy_chart/store/utils.ts b/src/chart_types/xy_chart/store/utils.ts index eb57d600b2..b4c3e2f637 100644 --- a/src/chart_types/xy_chart/store/utils.ts +++ b/src/chart_types/xy_chart/store/utils.ts @@ -36,6 +36,8 @@ import { LineSeriesSpec, Rotation, isBandedSpec, + Fit, + FitConfig, } from '../utils/specs'; import { ColorConfig, Theme } from '../../../utils/themes/theme'; import { identity, mergePartial } from '../../../utils/commons'; @@ -165,7 +167,13 @@ export function computeSeriesDomains( const xDomain = mergeXDomain(specsArray, xValues, customXDomain); const yDomain = mergeYDomain(splittedSeries, specsArray, customYDomainsByGroupId); - const formattedDataSeries = getFormattedDataseries(specsArray, splittedSeries, xValues, xDomain.scaleType); + const formattedDataSeries = getFormattedDataseries( + specsArray, + splittedSeries, + xValues, + xDomain.scaleType, + seriesSpecs, + ); // we need to get the last values from the formatted dataseries // because we change the format if we are on percentage mode @@ -493,6 +501,7 @@ export function renderGeometries( xScaleOffset, lineSeriesStyle, spec.pointStyleAccessor, + Boolean(spec.fit && ((spec.fit as FitConfig).type || spec.fit) !== Fit.None), ); lineGeometriesIndex = mergeGeometriesIndexes(lineGeometriesIndex, renderedLines.indexedGeometries); lines.push(renderedLines.lineGeometry); @@ -520,6 +529,7 @@ export function renderGeometries( areaSeriesStyle, isStacked, spec.pointStyleAccessor, + Boolean(spec.fit && ((spec.fit as FitConfig).type || spec.fit) !== Fit.None), ); areaGeometriesIndex = mergeGeometriesIndexes(areaGeometriesIndex, renderedAreas.indexedGeometries); areas.push(renderedAreas.areaGeometry); diff --git a/src/chart_types/xy_chart/utils/fit_function.test.ts b/src/chart_types/xy_chart/utils/fit_function.test.ts new file mode 100644 index 0000000000..2cd40e95a8 --- /dev/null +++ b/src/chart_types/xy_chart/utils/fit_function.test.ts @@ -0,0 +1,1002 @@ +import { + MockDataSeries, + getFilledNullData, + getFilledNonNullData, + getYResolvedData, + MockDataSeriesDatum, +} from '../../../mocks'; +import { Fit } from './specs'; +import { ScaleType } from '../../../utils/scales/scales'; +import { DataSeries } from './series'; + +import * as seriesUtils from './stacked_series_utils'; +import * as testModule from './fit_function'; + +describe('Fit Function', () => { + describe('getValue', () => { + describe('Non-Ordinal scale', () => { + it('should return current datum if next and previous are null with no endValue', () => { + const current = MockDataSeriesDatum.default(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average); + + expect(actual).toBe(current); + expect(actual.filled).toBeUndefined(); + }); + + it('should return current datum with filled endValue if next and previous are null with endValue', () => { + const current = MockDataSeriesDatum.default(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100); + + expect(actual.filled!.y1).toBe(100); + }); + + describe('previous is not null and fit type is Carry', () => { + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Carry); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('next is not null and fit type is Lookahead', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Lookahead); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('current and previous datums are not null', () => { + describe('Average - fit type', () => { + it('should return current datum with average values from previous and next', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Average); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: (10 + 20) / 2, + }); + }); + }); + + describe('Nearest - fit type', () => { + it('should return current datum with values from previous not next', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 10, + }); + }); + + it('should return current datum with values from next not previous', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 9 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('Linear - fit type', () => { + it('should return average from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 5 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: (10 + 20) / 2, + }); + }); + + it('should return positive slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 10 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 20 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 13, + }); + }); + + it('should return negative slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 10, y1: 10 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 17, + }); + }); + + it('should return complex interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 0.767, y1: 10.545 }); + const current = MockDataSeriesDatum.simple({ x: 3.564 }); + const next = MockDataSeriesDatum.full({ x: 10.767, y1: 20.657 }); + const actual = testModule.getValue(current, 0, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 13.3733264, + }); + }); + }); + }); + + describe('next or previous datums are not null - with fits requring bounding datums', () => { + describe('Nearest - fit type', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe("endValue is set to 'nearest'", () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 3 }); + const next = MockDataSeriesDatum.full({ x: 4, y1: 20 }); + const actual = testModule.getValue(current, 0, null, next, Fit.Average, 'nearest'); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 0, y1: 20 }); + const current = MockDataSeriesDatum.simple({ x: 3 }); + const actual = testModule.getValue(current, 0, previous, null, Fit.Average, 'nearest'); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + }); + }); + describe('Ordinal scale', () => { + it('should return current datum if next and previous are null with no endValue', () => { + const current = MockDataSeriesDatum.ordinal(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average); + + expect(actual).toBe(current); + expect(actual.filled).toBeUndefined(); + }); + + it('should return current datum with filled endValue if next and previous are null with endValue', () => { + const current = MockDataSeriesDatum.ordinal(); + const actual = testModule.getValue(current, 0, null, null, Fit.Average, 100); + + expect(actual.filled!.y1).toBe(100); + }); + + describe('previous is not null and fit type is Carry', () => { + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Carry); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('next is not null and fit type is Lookahead', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Lookahead); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('current and previous datums are not null', () => { + describe('Average - fit type', () => { + it('should return current datum with average values from previous and next', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Average); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: (10 + 20) / 2, + }); + }); + }); + + describe('Nearest - fit type', () => { + it('should return current datum with values from previous not next', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 10, + }); + }); + + it('should return current datum with values from next not previous', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 9, previous, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe('Linear - fit type', () => { + it('should return average from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 5, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: (10 + 20) / 2, + }); + }); + + it('should return positive slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 10, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 13, + }); + }); + + it('should return negative slope interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 10, fittingIndex: 10 }); + const actual = testModule.getValue(current, 3, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 17, + }); + }); + + it('should return complex interpolated value from equidistant previous and next datums', () => { + const previous = MockDataSeriesDatum.full({ x: 'a', fittingIndex: 0.767, y1: 10.545 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', fittingIndex: 10.767, y1: 20.657 }); + const actual = testModule.getValue(current, 3.564, previous, next, Fit.Linear); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 13.3733264, + }); + }); + }); + }); + + describe('next or previous datums are not null - with fits requring bounding datums', () => { + describe('Nearest - fit type', () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Nearest); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + + describe("endValue is set to 'nearest'", () => { + it('should return current datum with value from next when previous is null', () => { + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const next = MockDataSeriesDatum.full({ x: 'e', y1: 20, fittingIndex: 4 }); + const actual = testModule.getValue(current, 3, null, next, Fit.Average, 'nearest'); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + + it('should return current datum with value from next when previous is null', () => { + const previous = MockDataSeriesDatum.full({ x: 4, y1: 20, fittingIndex: 0 }); + const current = MockDataSeriesDatum.simple({ x: 'c' }); + const actual = testModule.getValue(current, 3, previous, null, Fit.Average, 'nearest'); + + expect(actual).toMatchObject(current); + expect(actual.filled).toEqual({ + y1: 20, + }); + }); + }); + }); + }); + }); + + describe('parseConfig', () => { + it('should return default type when none exists', () => { + const actual = testModule.parseConfig(); + + expect(actual).toEqual({ + type: Fit.None, + }); + }); + + it('should parse string config', () => { + const actual = testModule.parseConfig(Fit.Average); + + expect(actual).toEqual({ + type: Fit.Average, + }); + }); + + it('should return default when Explicit is passes without value', () => { + const actual = testModule.parseConfig({ type: Fit.Explicit }); + + expect(actual).toEqual({ + type: Fit.None, + }); + }); + + it('should return type and value when Explicit is passes with value', () => { + const actual = testModule.parseConfig({ type: Fit.Explicit, value: 20 }); + + expect(actual).toEqual({ + type: Fit.Explicit, + value: 20, + endValue: undefined, + }); + }); + + it('should return type when no value or endValue is given', () => { + const actual = testModule.parseConfig({ type: Fit.Average }); + + expect(actual).toEqual({ + type: Fit.Average, + value: undefined, + endValue: undefined, + }); + }); + + it('should return type and endValue when endValue is passed', () => { + const actual = testModule.parseConfig({ type: Fit.Average, endValue: 20 }); + + expect(actual).toEqual({ + type: Fit.Average, + value: undefined, + endValue: 20, + }); + }); + }); + + describe('fitFunction', () => { + let dataSeries: DataSeries; + + beforeAll(() => { + jest.spyOn(testModule, 'parseConfig'); + jest.spyOn(testModule, 'getValue'); + dataSeries = MockDataSeries.fitFunction(); + }); + + describe('allow mutliple fit config types', () => { + it('should allow string config', () => { + testModule.fitFunction(dataSeries, Fit.None, ScaleType.Linear); + + expect(testModule.parseConfig).toHaveBeenCalledWith(Fit.None); + expect(testModule.parseConfig).toHaveBeenCalledTimes(1); + }); + + it('should allow object config', () => { + const fitConfig = { + type: Fit.None, + }; + testModule.fitFunction(dataSeries, fitConfig, ScaleType.Linear); + + expect(testModule.parseConfig).toHaveBeenCalledWith(fitConfig); + expect(testModule.parseConfig).toHaveBeenCalledTimes(1); + }); + }); + + describe('sorting', () => { + const spies: jest.SpyInstance[] = []; + const mockArray: any[] = []; + // @ts-ignore + jest.spyOn(mockArray, 'sort'); + + beforeAll(() => { + // @ts-ignore + spies.push(jest.spyOn(dataSeries.data, 'sort')); + // @ts-ignore + spies.push(jest.spyOn(dataSeries.data, 'slice').mockReturnValue(mockArray)); + }); + + afterAll(() => { + spies.forEach((s) => s.mockRestore()); + }); + + it('should call splice sort only', () => { + testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear); + + expect(dataSeries.data.sort).not.toHaveBeenCalled(); + expect(dataSeries.data.slice).toHaveBeenCalledTimes(1); + expect(mockArray.sort).toHaveBeenCalledTimes(1); + }); + + it('should not call splice.sort if sorted is true', () => { + testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear, true); + + expect(dataSeries.data.slice).not.toHaveBeenCalled(); + expect(mockArray.sort).not.toHaveBeenCalled(); + }); + + it('should not call splice.sort if scale is ordinal', () => { + testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Ordinal); + + expect(dataSeries.data.slice).not.toHaveBeenCalled(); + expect(mockArray.sort).not.toHaveBeenCalled(); + }); + + it('should call splice.sort with predicate', () => { + jest.spyOn(seriesUtils, 'datumXSortPredicate'); + testModule.fitFunction(dataSeries, Fit.Linear, ScaleType.Linear); + + expect(seriesUtils.datumXSortPredicate).toHaveBeenCalledWith(Fit.Linear); + }); + }); + + describe.each([ScaleType.Linear, ScaleType.Ordinal])('ScaleType - %s', (scaleType) => { + const ordinal = scaleType === ScaleType.Ordinal; + + beforeAll(() => { + dataSeries = MockDataSeries.fitFunction({ ordinal }); + }); + + describe('EndValues', () => { + const sortedDS = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + + describe('number value', () => { + const endValue = 100; + it('should set end values - None', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.None, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toBeNull(); + expect(finalValues[finalValues.length - 1]).toBeNull(); + }); + + it('should set end values - Zero', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.Zero, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(0); + expect(finalValues[finalValues.length - 1]).toEqual(0); + }); + + it('should set end values - Explicit', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.Explicit, value: 20, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(20); + expect(finalValues[finalValues.length - 1]).toEqual(20); + }); + + it('should set end values - Lookahead', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Lookahead, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + + it('should set end values - Nearest', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Nearest, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Average', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Average, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(endValue); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + + it('should set end values - Linear', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Linear, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(endValue); + expect(finalValues[finalValues.length - 1]).toEqual(endValue); + }); + }); + + describe("'nearest' value", () => { + const endValue = 'nearest'; + + it('should set end values - None', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.None, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toBeNull(); + expect(finalValues[finalValues.length - 1]).toBeNull(); + }); + + it('should set end values - Zero', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.Zero, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(0); + expect(finalValues[finalValues.length - 1]).toEqual(0); + }); + + it('should set end values - Explicit', () => { + const actual = testModule.fitFunction(sortedDS, { type: Fit.Explicit, value: 20, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(20); + expect(finalValues[finalValues.length - 1]).toEqual(20); + }); + + it('should set end values - Lookahead', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Lookahead, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Nearest', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Nearest, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Average', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Average, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + + it('should set end values - Linear', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Linear, endValue }, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues[0]).toEqual(3); + expect(finalValues[finalValues.length - 1]).toEqual(12); + }); + }); + }); + + describe('Fit Types', () => { + describe('None', () => { + it('should return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.None, scaleType); + + expect(actual).toBe(dataSeries); + }); + + it('should return null data values without fit', () => { + const actual = testModule.fitFunction(dataSeries, Fit.None, scaleType); + + expect(getFilledNullData(actual.data)).toEqualArrayOf(undefined, 7); + }); + }); + + describe('Zero', () => { + it('should NOT return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Zero, scaleType); + + expect(actual).not.toBe(dataSeries); + }); + + it('should return null data values with zeros', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Zero, scaleType); + const testActual = getFilledNullData(actual.data); + + expect(testActual).toEqualArrayOf(0, 7); + }); + }); + + describe('Explicit', () => { + it('should return original dataSeries if no value provided', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Explicit }, scaleType); + + expect(actual).toBe(dataSeries); + }); + + it('should return null data values with set value', () => { + const actual = testModule.fitFunction(dataSeries, { type: Fit.Explicit, value: 20 }, scaleType); + const testActual = getFilledNullData(actual.data); + + expect(testActual).toEqualArrayOf(20, 7); + }); + }); + + describe('Lookahead', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType); + + expect(actual).not.toBe(dataSeries); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType); + + expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Lookahead, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds, Fit.Lookahead, scaleType); + const previous = actual.data[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Lookahead, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Lookahead, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType); + const length = getFilledNullData(actual.data).length; + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Lookahead, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues).toEqual([3, 3, 5, 4, 4, 5, 5, 6, 12, 12, 12, 12, null]); + }); + }); + describe('Nearest', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType); + + expect(actual).not.toBe(dataSeries); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType); + + expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Nearest, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds, Fit.Nearest, scaleType); + const previous = actual.data[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Nearest, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Nearest, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType); + const length = getFilledNullData(actual.data).length; + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Nearest, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues).toEqual([3, 3, 5, 5, 4, 4, 5, 6, 6, 6, 12, 12, 12]); + }); + }); + + describe('Average', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType); + + expect(actual).not.toBe(dataSeries); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType); + + expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Average, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Average, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds, Fit.Average, scaleType); + const previous = actual.data[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Average, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Average, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Average, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType); + const length = getFilledNullData(actual.data).length; + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Average, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 9, 9, 9, 12, null]); + }); + }); + + describe('Linear', () => { + it('should not return original dataSeries', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType); + + expect(actual).not.toBe(dataSeries); + }); + + it('should not fill non-null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType); + + expect(getFilledNonNullData(actual.data)).toEqualArrayOf(undefined, 6); + }); + + it('should call getValue for first datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Linear, scaleType); + const [current, next] = ds.data; + + expect(testModule.getValue).nthCalledWith( + 1, + expect.objectContaining(current), + 0, + null, + expect.objectContaining(next), + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for 10th (4th null) datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + const actual = testModule.fitFunction(ds, Fit.Linear, scaleType); + const previous = actual.data[7]; + const current = ds.data[8]; + const next = ds.data[11]; + + expect(testModule.getValue).nthCalledWith( + 4, + expect.objectContaining(current), + 8, + expect.objectContaining(previous), + expect.objectContaining(next), + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for last datum with correct args', () => { + const ds = MockDataSeries.fitFunction({ ordinal, shuffle: false }); + testModule.fitFunction(ds, Fit.Linear, scaleType); + const [current, previous] = ds.data.slice().reverse(); + + expect(testModule.getValue).lastCalledWith( + expect.objectContaining(current), + 12, + expect.objectContaining(previous), + null, + Fit.Linear, + undefined, + ); + }); + + it('should call getValue for only null values', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType); + const length = getFilledNullData(actual.data).length; + + expect(testModule.getValue).toBeCalledTimes(length); + }); + + it('should fill null values correctly', () => { + const actual = testModule.fitFunction(dataSeries, Fit.Linear, scaleType); + const finalValues = getYResolvedData(actual.data); + + expect(finalValues).toEqual([null, 3, 5, 4.5, 4, 4.5, 5, 6, 7.5, 9, 10.5, 12, null]); + }); + }); + }); + }); + }); +}); diff --git a/src/chart_types/xy_chart/utils/fit_function.ts b/src/chart_types/xy_chart/utils/fit_function.ts new file mode 100644 index 0000000000..6e3f1bd818 --- /dev/null +++ b/src/chart_types/xy_chart/utils/fit_function.ts @@ -0,0 +1,230 @@ +import { DeepNonNullable } from 'utility-types'; + +import { Fit, FitConfig } from './specs'; +import { DataSeries, DataSeriesDatum } from './series'; +import { datumXSortPredicate } from './stacked_series_utils'; +import { ScaleType } from '../../../utils/scales/scales'; + +/** + * Fit type that requires previous and/or next `non-nullable` values + */ +export type BoundingFit = Exclude; + +/** + * `DataSeriesDatum` with non-`null` value for `x` and `y1` + */ +export type FullDataSeriesDatum = Omit & + DeepNonNullable>; + +/** + * Embellishes `FullDataSeriesDatum` with `fittingIndex` for ordinal scales + */ +export type WithIndex = T & { fittingIndex: number }; + +/** + * Returns `[x, y1]` values for a given datum with `fittingIndex` + */ +export const getXYValues = ({ x, y1, fittingIndex }: WithIndex): [number, number] => { + return [typeof x === 'string' ? fittingIndex : x, y1]; +}; + +export const getValue = ( + current: DataSeriesDatum, + currentIndex: number, + previous: WithIndex | null, + next: WithIndex | null, + type: BoundingFit, + endValue?: number | 'nearest', +): DataSeriesDatum => { + if (previous !== null && type === Fit.Carry) { + return { + ...current, + filled: { + y1: previous.y1, + }, + }; + } else if (next !== null && type === Fit.Lookahead) { + return { + ...current, + filled: { + y1: next.y1, + }, + }; + } else if (previous !== null && next !== null) { + if (type === Fit.Average) { + return { + ...current, + filled: { + y1: (previous.y1 + next.y1) / 2, + }, + }; + } else if (current.x !== null && previous.x !== null && next.x !== null) { + const [x1, y1] = getXYValues(previous); + const [x2, y2] = getXYValues(next); + const currentX = typeof current.x === 'string' ? currentIndex : current.x; + + if (type === Fit.Nearest) { + const x1Delta = Math.abs(currentX - x1); + const x2Delta = Math.abs(currentX - x2); + return { + ...current, + filled: { + y1: x1Delta > x2Delta ? y2 : y1, + }, + }; + } else if (type === Fit.Linear) { + return { + ...current, + filled: { + // simple linear interpolation function + y1: previous.y1 + (currentX - x1) * ((y2 - y1) / (x2 - x1)), + }, + }; + } + } + } else if ((previous !== null || next !== null) && (type === Fit.Nearest || endValue === 'nearest')) { + return { + ...current, + filled: { + y1: previous !== null ? previous.y1 : next!.y1, + }, + }; + } + + if (endValue === undefined || typeof endValue === 'string') { + return current; + } + + // No matching fit - should only fall here on end conditions + return { + ...current, + filled: { + y1: endValue, + }, + }; +}; + +export const parseConfig = (config?: Exclude | FitConfig): FitConfig => { + if (!config) { + return { + type: Fit.None, + }; + } + + if (typeof config === 'string') { + return { + type: config, + }; + } + + if (config.type === Fit.Explicit && config.value === undefined) { + // Using explicit fit function requires a value + return { + type: Fit.None, + }; + } + + return { + type: config.type, + value: config.type === Fit.Explicit ? config.value : undefined, + endValue: config.endValue, + }; +}; + +export const fitFunction = ( + dataSeries: DataSeries, + fitConfig: Exclude | FitConfig, + xScaleType: ScaleType, + sorted = false, +): DataSeries => { + const { type, value, endValue } = parseConfig(fitConfig); + + if (type === Fit.None) { + return dataSeries; + } + + const { data } = dataSeries; + + if (type === Fit.Zero) { + return { + ...dataSeries, + data: data.map((datum) => ({ + ...datum, + filled: { + y1: datum.y1 === null ? 0 : undefined, + }, + })), + }; + } + + if (type === Fit.Explicit) { + if (value === undefined) { + return dataSeries; + } + + return { + ...dataSeries, + data: data.map((datum) => ({ + ...datum, + filled: { + y1: datum.y1 === null ? value : undefined, + }, + })), + }; + } + + const sortedData = + sorted || xScaleType === ScaleType.Ordinal ? data : data.slice().sort(datumXSortPredicate(xScaleType)); + const newData: DataSeriesDatum[] = []; + let previousNonNullDatum: WithIndex | null = null; + let nextNonNullDatum: WithIndex | null = null; + + for (let i = 0; i < sortedData.length; i++) { + let j = i; + const current = sortedData[i]; + + if ( + current.y1 === null && + nextNonNullDatum === null && + (type === Fit.Lookahead || + type === Fit.Nearest || + type === Fit.Average || + type === Fit.Linear || + endValue === 'nearest') + ) { + // Forward lookahead to get next non-null value + for (j = i + 1; j < sortedData.length; j++) { + const value = sortedData[j]; + + if (value.y1 !== null && value.x !== null) { + nextNonNullDatum = { + ...(value as FullDataSeriesDatum), + fittingIndex: j, + }; + break; + } + } + } + + const newValue = + current.y1 === null ? getValue(current, i, previousNonNullDatum, nextNonNullDatum, type, endValue) : current; + + newData[i] = newValue; + + if (current.y1 !== null && current.x !== null) { + previousNonNullDatum = { + ...(current as FullDataSeriesDatum), + fittingIndex: i, + }; + } + + if (nextNonNullDatum !== null && nextNonNullDatum.x <= current.x) { + nextNonNullDatum = null; + } + } + + return { + ...dataSeries, + data: newData, + }; +}; diff --git a/src/chart_types/xy_chart/utils/interactions.ts b/src/chart_types/xy_chart/utils/interactions.ts index 958c5aeb92..ec5ea633d8 100644 --- a/src/chart_types/xy_chart/utils/interactions.ts +++ b/src/chart_types/xy_chart/utils/interactions.ts @@ -1,3 +1,5 @@ +import { $Values } from 'utility-types'; + import { BarGeometry, IndexedGeometry, isBarGeometry, isPointGeometry, PointGeometry } from '../rendering/rendering'; import { Datum, Rotation } from './specs'; import { Dimensions } from '../../../utils/dimensions'; @@ -15,11 +17,7 @@ export const TooltipType = Object.freeze({ None: 'none' as 'none', }); -export type TooltipType = - | typeof TooltipType.VerticalCursor - | typeof TooltipType.Crosshairs - | typeof TooltipType.Follow - | typeof TooltipType.None; +export type TooltipType = $Values; export interface TooltipValue { name: string; diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts index 9a72963522..d45235bab8 100644 --- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.test.ts @@ -1,6 +1,12 @@ import { getSpecId } from '../../../utils/ids'; -import { formatNonStackedDataSeriesValues } from './nonstacked_series_utils'; import { RawDataSeries } from './series'; +import { ScaleType } from '../../../utils/scales/scales'; +import { MockRawDataSeries, MockDataSeries } from '../../../mocks'; +import { MockSeriesSpecs, MockSeriesSpec } from '../../../mocks/specs'; + +import * as fitFunctionModule from './fit_function'; +import * as testModule from './nonstacked_series_utils'; +import { Fit } from './specs'; const EMPTY_DATA_SET: RawDataSeries[] = [ { @@ -172,11 +178,21 @@ const DATA_SET_WITH_NULL_2: RawDataSeries[] = [ describe('Non-Stacked Series Utils', () => { describe('Format stacked dataset', () => { test('empty data', () => { - const formattedData = formatNonStackedDataSeriesValues(EMPTY_DATA_SET, false); + const formattedData = testModule.formatNonStackedDataSeriesValues( + EMPTY_DATA_SET, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[0].data.length).toBe(0); }); test('format data without nulls', () => { - let formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET, false); + let formattedData = testModule.formatNonStackedDataSeriesValues( + STANDARD_DATA_SET, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -201,7 +217,12 @@ describe('Non-Stacked Series Utils', () => { y0: 0, y1: 30, }); - formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET, true); + formattedData = testModule.formatNonStackedDataSeriesValues( + STANDARD_DATA_SET, + true, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -228,7 +249,12 @@ describe('Non-Stacked Series Utils', () => { }); }); test('format data with nulls', () => { - const formattedData = formatNonStackedDataSeriesValues(WITH_NULL_DATASET, false); + const formattedData = testModule.formatNonStackedDataSeriesValues( + WITH_NULL_DATASET, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[1].data[0]).toEqual({ datum: undefined, initialY0: null, @@ -239,7 +265,12 @@ describe('Non-Stacked Series Utils', () => { }); }); test('format data without nulls with y0 values', () => { - const formattedData = formatNonStackedDataSeriesValues(STANDARD_DATA_SET_WY0, false); + const formattedData = testModule.formatNonStackedDataSeriesValues( + STANDARD_DATA_SET_WY0, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: 2, @@ -266,7 +297,12 @@ describe('Non-Stacked Series Utils', () => { }); }); test('format data with nulls', () => { - const formattedData = formatNonStackedDataSeriesValues(WITH_NULL_DATASET_WY0, false); + const formattedData = testModule.formatNonStackedDataSeriesValues( + WITH_NULL_DATASET_WY0, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData[0].data[0]).toEqual({ datum: undefined, initialY0: 2, @@ -293,7 +329,12 @@ describe('Non-Stacked Series Utils', () => { }); }); test('format data without nulls on second series', () => { - const formattedData = formatNonStackedDataSeriesValues(DATA_SET_WITH_NULL_2, false); + const formattedData = testModule.formatNonStackedDataSeriesValues( + DATA_SET_WITH_NULL_2, + false, + MockSeriesSpecs.empty(), + ScaleType.Linear, + ); expect(formattedData.length).toBe(2); expect(formattedData[0].data.length).toBe(3); expect(formattedData[1].data.length).toBe(2); @@ -340,4 +381,76 @@ describe('Non-Stacked Series Utils', () => { }); }); }); + + describe('Using fit functions', () => { + describe.each(['area', 'line'])('Spec type - %s', (specType) => { + const rawDataSeries = [MockRawDataSeries.fitFunction({ shuffle: false })]; + const dataSeries = MockDataSeries.fitFunction({ shuffle: false }); + const spec = + specType === 'area' ? MockSeriesSpec.area({ fit: Fit.Linear }) : MockSeriesSpec.line({ fit: Fit.Linear }); + const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]); + + beforeAll(() => { + jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeries); + jest.spyOn(testModule, 'formatNonStackedDataValues').mockReturnValue(dataSeries); + }); + + it('return call formatNonStackedDataValues with args', () => { + testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(testModule.formatNonStackedDataValues).toHaveBeenCalledWith(rawDataSeries[0], false); + }); + + it('return call fitFunction with args', () => { + testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).toHaveBeenCalledWith(dataSeries, Fit.Linear, ScaleType.Linear); + }); + + it('return not call fitFunction if no fit specified', () => { + const spec = + specType === 'area' ? MockSeriesSpec.area({ fit: undefined }) : MockSeriesSpec.line({ fit: undefined }); + const noFitSpec = MockSeriesSpecs.fromSpecs([spec]); + testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, noFitSpec, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled(); + }); + + it('return fitted dataSeries', () => { + const actual = testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(actual[0]).toBe(dataSeries); + }); + }); + + describe('Non area and line specs', () => { + const rawDataSeries = [MockRawDataSeries.fitFunction({ shuffle: false })]; + const dataSeries = MockDataSeries.fitFunction({ shuffle: false }); + const spec = MockSeriesSpec.bar(); + const seriesSpecs = MockSeriesSpecs.fromSpecs([spec]); + + beforeAll(() => { + jest.spyOn(fitFunctionModule, 'fitFunction').mockReturnValue(dataSeries); + jest.spyOn(testModule, 'formatNonStackedDataValues').mockReturnValue(dataSeries); + }); + + it('return call formatNonStackedDataValues with args', () => { + testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(testModule.formatNonStackedDataValues).toHaveBeenCalledWith(rawDataSeries[0], false); + }); + + it('return call fitFunction with args', () => { + testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(fitFunctionModule.fitFunction).not.toHaveBeenCalled(); + }); + + it('return fitted dataSeries', () => { + const actual = testModule.formatNonStackedDataSeriesValues(rawDataSeries, false, seriesSpecs, ScaleType.Linear); + + expect(actual[0]).toBe(dataSeries); + }); + }); + }); }); diff --git a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts index cd767c013c..6b30fa3004 100644 --- a/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/nonstacked_series_utils.ts @@ -1,26 +1,44 @@ import { DataSeries, DataSeriesDatum, RawDataSeries } from './series'; +import { fitFunction } from './fit_function'; +import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs } from './specs'; +import { ScaleType } from '../../../utils/scales/scales'; -export function formatNonStackedDataSeriesValues(dataseries: RawDataSeries[], scaleToExtent: boolean): DataSeries[] { +export const formatNonStackedDataSeriesValues = ( + dataseries: RawDataSeries[], + scaleToExtent: boolean, + seriesSpecs: SeriesSpecs, + xScaleType: ScaleType, +): DataSeries[] => { const len = dataseries.length; - let i; const formattedValues: DataSeries[] = []; - for (i = 0; i < len; i++) { - const formattedValue = formatNonStackedDataValues(dataseries[i], scaleToExtent); - formattedValues.push(formattedValue); + for (let i = 0; i < len; i++) { + const formattedDataValue = formatNonStackedDataValues(dataseries[i], scaleToExtent); + const spec = seriesSpecs.get(formattedDataValue.specId); + + if ( + spec !== null && + spec !== undefined && + (isAreaSeriesSpec(spec) || isLineSeriesSpec(spec)) && + spec.fit !== undefined + ) { + const fittedData = fitFunction(formattedDataValue, spec.fit, xScaleType); + formattedValues.push(fittedData); + } else { + formattedValues.push(formattedDataValue); + } } return formattedValues; -} +}; -export function formatNonStackedDataValues(dataSeries: RawDataSeries, scaleToExtent: boolean): DataSeries { +export const formatNonStackedDataValues = (dataSeries: RawDataSeries, scaleToExtent: boolean): DataSeries => { const len = dataSeries.data.length; - let i; const formattedValues: DataSeries = { key: dataSeries.key, specId: dataSeries.specId, seriesColorKey: dataSeries.seriesColorKey, data: [], }; - for (i = 0; i < len; i++) { + for (let i = 0; i < len; i++) { const data = dataSeries.data[i]; const { x, y1, datum } = data; let y0: number | null; @@ -45,4 +63,4 @@ export function formatNonStackedDataValues(dataSeries: RawDataSeries, scaleToExt formattedValues.data.push(formattedValue); } return formattedValues; -} +}; diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 9e6787ef1f..ba284c5540 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -360,6 +360,7 @@ describe('Series', () => { splittedDataSeries.splittedSeries, xValues, ScaleType.Linear, + new Map(), ); expect(stackedDataSeries.stacked).toMatchSnapshot(); }); diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index 3627244ea7..9a68fde443 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -11,14 +11,14 @@ import { ScaleType } from '../../../utils/scales/scales'; export interface FilledValues { /** the x value */ - x: number | string; + x?: number | string; /** the max y value */ - y1: number | null; + y1?: number; /** the minimum y value */ - y0: number | null; + y0?: number; } -export interface RawDataSeriesDatum { +export interface RawDataSeriesDatum { /** the x value */ x: number | string; /** the main y metric */ @@ -26,10 +26,10 @@ export interface RawDataSeriesDatum { /** the optional y0 metric, used for bars or area with a lower bound */ y0?: number | null; /** the datum */ - datum?: any; + datum?: T; } -export interface DataSeriesDatum { +export interface DataSeriesDatum { /** the x value */ x: number | string; /** the max y value */ @@ -40,10 +40,10 @@ export interface DataSeriesDatum { initialY1: number | null; /** initial y0 value, non stacked */ initialY0: number | null; - /** the datum */ - datum?: any; + /** initial datum */ + datum?: T; /** the list of filled values because missing or nulls */ - filled?: Partial; + filled?: FilledValues; } export interface DataSeries { @@ -218,6 +218,7 @@ export function getFormattedDataseries( dataSeries: Map, xValues: Set, xScaleType: ScaleType, + seriesSpecs: Map, ): { stacked: FormattedDataSeries[]; nonStacked: FormattedDataSeries[]; @@ -258,7 +259,7 @@ export function getFormattedDataseries( nonStackedFormattedDataSeries.push({ groupId, counts: nonStackedDataSeries.counts, - dataSeries: formatNonStackedDataSeriesValues(nonStackedDataSeries.rawDataSeries, false), + dataSeries: formatNonStackedDataSeriesValues(nonStackedDataSeries.rawDataSeries, false, seriesSpecs, xScaleType), }); }); return { diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index db02c886d6..999539b653 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -8,13 +8,14 @@ import { PointStyle, } from '../../../utils/themes/theme'; import { Accessor, AccessorFormat } from '../../../utils/accessor'; -import { Omit, RecursivePartial } from '../../../utils/commons'; +import { RecursivePartial } from '../../../utils/commons'; import { AnnotationId, AxisId, GroupId, SpecId } from '../../../utils/ids'; import { ScaleContinuousType, ScaleType } from '../../../utils/scales/scales'; import { CurveType } from '../../../utils/curves'; import { DataSeriesColorsValues, RawDataSeriesDatum } from './series'; import { GeometryId } from '../rendering/rendering'; import { AnnotationTooltipFormatter } from '../annotations/annotation_utils'; +import { $Values } from 'utility-types'; export type Datum = any; export type Rotation = 0 | 90 | -90 | 180; @@ -54,6 +55,90 @@ interface DomainMinInterval { minInterval?: number; } +/** + * The fit function type + */ +export const Fit = Object.freeze({ + /** + * Don't draw value on the graph. Slices out area between `null` values. + * + * Example: + * ```js + * [2, null, null, 8] => [2, null null, 8] + * ``` + */ + None: 'none' as 'none', + /** + * Use the previous non-`null` value + * + * Example: + * ```js + * [2, null, null, 8] => [2, 2, 2, 8] + * ``` + * + * @opposite `Lookahead` + */ + Carry: 'carry' as 'carry', + /** + * Use the next non-`null` value + * + * Example: + * ```js + * [2, null, null, 8] => [2, 8, 8, 8] + * ``` + * + * @opposite `Carry` + */ + Lookahead: 'lookahead' as 'lookahead', + /** + * Use the closest non-`null` value (before or after) + * + * Example: + * ```js + * [2, null, null, 8] => [2, 2, 8, 8] + * ``` + */ + Nearest: 'nearest' as 'nearest', + /** + * Average between the closest non-`null` values + * + * Example: + * ```js + * [2, null, null, 8] => [2, 5, 5, 8] + * ``` + */ + Average: 'average' as 'average', + /** + * Linear interpolation between the closest non-`null` values + * + * Example: + * ```js + * [2, null, null, 8] => [2, 4, 6, 8] + * ``` + */ + Linear: 'linear' as 'linear', + /** + * Sets all `null` values to `0` + * + * Example: + * ```js + * [2, null, null, 8] => [2, 0, 0, 8] + * ``` + */ + Zero: 'zero' as 'zero', + /** + * Specify an explicit value `X` + * + * Example: + * ```js + * [2, null, null, 8] => [2, X, X, 8] + * ``` + */ + Explicit: 'explicit' as 'explicit', +}); + +export type Fit = $Values; + interface LowerBound { /** Lower bound of domain range */ min: number; @@ -179,6 +264,8 @@ export interface SeriesScales { export type BasicSeriesSpec = SeriesSpec & SeriesAccessors & SeriesScales; +export type SeriesSpecs = Map; + /** * This spec describe the dataset configuration used to display a bar series. */ @@ -215,6 +302,25 @@ export type HistogramBarSeriesSpec = Omit & { enableHistogramMode: true; }; +export type FitConfig = { + /** + * Fit type for data with null values + */ + type: Fit; + /** + * Fit value used when `type` is set to `Fit.Explicit` + */ + value?: number; + /** + * Value used for first and last point if fitting is not possible + * + * `'nearest'` will set indeterminate end values to the closes _visible_ point. + * + * Note: Computed fit values will always take precedence over `endValues` + */ + endValue?: number | 'nearest'; +}; + /** * This spec describe the dataset configuration used to display a line series. */ @@ -228,6 +334,10 @@ export type LineSeriesSpec = BasicSeriesSpec & * An optional functional accessor to return custom color or style for point datum */ pointStyleAccessor?: PointStyleAccessor; + /** + * Fit config to fill `null` values in dataset + */ + fit?: Exclude | FitConfig; }; /** @@ -249,6 +359,10 @@ export type AreaSeriesSpec = BasicSeriesSpec & * An optional functional accessor to return custom color or style for point datum */ pointStyleAccessor?: PointStyleAccessor; + /** + * Fit config to fill `null` values in dataset + */ + fit?: Exclude | FitConfig; }; interface HistogramConfig { @@ -330,7 +444,7 @@ export const Position = Object.freeze({ Right: 'right' as 'right', }); -export type Position = typeof Position.Top | typeof Position.Bottom | typeof Position.Left | typeof Position.Right; +export type Position = $Values; export const AnnotationTypes = Object.freeze({ Line: 'line' as 'line', @@ -338,17 +452,14 @@ export const AnnotationTypes = Object.freeze({ Text: 'text' as 'text', }); -export type AnnotationType = - | typeof AnnotationTypes.Line - | typeof AnnotationTypes.Rectangle - | typeof AnnotationTypes.Text; +export type AnnotationType = $Values; export const AnnotationDomainTypes = Object.freeze({ XDomain: 'xDomain' as 'xDomain', YDomain: 'yDomain' as 'yDomain', }); -export type AnnotationDomainType = typeof AnnotationDomainTypes.XDomain | typeof AnnotationDomainTypes.YDomain; +export type AnnotationDomainType = $Values; export interface LineAnnotationDatum { dataValue: any; diff --git a/src/chart_types/xy_chart/utils/stacked_series_utils.ts b/src/chart_types/xy_chart/utils/stacked_series_utils.ts index e021ce791c..53d5b5bb33 100644 --- a/src/chart_types/xy_chart/utils/stacked_series_utils.ts +++ b/src/chart_types/xy_chart/utils/stacked_series_utils.ts @@ -7,6 +7,13 @@ interface StackedValues { total: number; } +export const datumXSortPredicate = (xScaleType: ScaleType) => (a: DataSeriesDatum, b: DataSeriesDatum) => { + if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') { + return 0; + } + return a.x - b.x; +}; + /** * Map each y value from a RawDataSeries on it's specific x value into, * ordering the stack based on the dataseries index. @@ -134,12 +141,7 @@ export function formatStackedDataSeriesValues( newData.push(filledSeriesDatum); } } - newData.sort((a, b) => { - if (xScaleType === ScaleType.Ordinal || typeof a.x === 'string' || typeof b.x === 'string') { - return 0; - } - return a.x - b.x; - }); + newData.sort(datumXSortPredicate(xScaleType)); return { specId: ds.specId, key: ds.key, @@ -157,7 +159,7 @@ function getStackedFormattedSeriesDatum( seriesIndex: number, scaleToExtent: boolean, isPercentageMode = false, - filled?: Partial, + filled?: FilledValues, ): DataSeriesDatum | undefined { const { x, datum } = data; const stack = stackedValues.get(x); @@ -200,7 +202,7 @@ function getStackedFormattedSeriesDatum( stackedY1 = y1 !== null ? stackY + y1 : null; stackedY0 = y0 != null ? stackY + y0 : stackY; // configure null y0 if y1 is null - // it's semantically right to say y0 is null if y1 is null + // it's semantically correct to say y0 is null if y1 is null if (stackedY1 === null) { stackedY0 = null; } diff --git a/src/components/icons/icon.tsx b/src/components/icons/icon.tsx index fddea92545..66b2686bc0 100644 --- a/src/components/icons/icon.tsx +++ b/src/components/icons/icon.tsx @@ -8,8 +8,6 @@ import { EyeClosedIcon } from './assets/eye_closed'; import { ListIcon } from './assets/list'; import { QuestionInCircle } from './assets/question_in_circle'; -export type Omit = Pick>; - const typeToIconMap = { alert: AlertIcon, dot: DotIcon, diff --git a/src/components/react_canvas/area_geometries.tsx b/src/components/react_canvas/area_geometries.tsx index 618c3a206f..0ae752443c 100644 --- a/src/components/react_canvas/area_geometries.tsx +++ b/src/components/react_canvas/area_geometries.tsx @@ -1,6 +1,7 @@ -import { Group as KonvaGroup, ContainerConfig } from 'konva'; import React from 'react'; +import { Group as KonvaGroup, PathConfig } from 'konva'; import { Circle, Group, Path } from 'react-konva'; + import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { AreaGeometry, @@ -16,6 +17,8 @@ import { buildPointRenderProps, PointStyleProps, buildLineRenderProps, + Clippings, + clipRanges, } from './utils/rendering_props_utils'; import { mergePartial } from '../../utils/commons'; @@ -24,7 +27,7 @@ interface AreaGeometriesDataProps { areas: AreaGeometry[]; sharedStyle: SharedGeometryStateStyle; highlightedLegendItem: LegendItem | null; - clippings: ContainerConfig; + clippings: Clippings; } interface AreaGeometriesDataState { overPoint?: PointGeometry; @@ -70,12 +73,26 @@ export class AreaGeometries extends React.PureComponent { - const { area, color, transform, geometryId, seriesAreaStyle } = glyph; + const { area, color, transform, geometryId, seriesAreaStyle, clippedRanges } = glyph; const geometryStateStyle = getGeometryStateStyle(geometryId, highlightedLegendItem, sharedStyle); const key = getGeometryIdKey(geometryId, 'area-'); const areaProps = buildAreaRenderProps(transform.x, area, color, seriesAreaStyle, geometryStateStyle); + + if (clippedRanges.length > 0) { + return ( + + + + + + + + + ); + } + return ( @@ -87,19 +104,39 @@ export class AreaGeometries extends React.PureComponent { - const { lines, color, geometryId, transform, seriesAreaLineStyle } = glyph; + const { lines, color, geometryId, transform, seriesAreaLineStyle, clippedRanges } = glyph; const geometryStateStyle = getGeometryStateStyle(geometryId, highlightedLegendItem, sharedStyle); const groupKey = getGeometryIdKey(geometryId, `area-line-${areaIndex}`); - const linesElements = lines.map((linePath, lineIndex) => { + const linesElementProps = lines.map<{ key: string; props: PathConfig }>((linePath, lineIndex) => { const key = getGeometryIdKey(geometryId, `area-line-${areaIndex}-${lineIndex}`); - const lineProps = buildLineRenderProps(transform.x, linePath, color, seriesAreaLineStyle, geometryStateStyle); - return ; + const props = buildLineRenderProps(transform.x, linePath, color, seriesAreaLineStyle, geometryStateStyle); + return { key, props }; }); + + if (clippedRanges.length > 0) { + return ( + + + {linesElementProps.map(({ key, props }) => ( + + ))} + + + {linesElementProps.map(({ key, props }) => ( + + ))} + + + ); + } + return ( - {...linesElements} + {linesElementProps.map(({ key, props }) => ( + + ))} ); }; diff --git a/src/components/react_canvas/bar_geometries.tsx b/src/components/react_canvas/bar_geometries.tsx index e7927313a8..646decef6b 100644 --- a/src/components/react_canvas/bar_geometries.tsx +++ b/src/components/react_canvas/bar_geometries.tsx @@ -1,18 +1,18 @@ -import { Group as KonvaGroup, ContainerConfig } from 'konva'; +import { Group as KonvaGroup } from 'konva'; import React from 'react'; import { Group, Rect } from 'react-konva'; import { animated, Spring } from 'react-spring/renderprops-konva.cjs'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; import { BarGeometry, getGeometryStateStyle } from '../../chart_types/xy_chart/rendering/rendering'; import { SharedGeometryStateStyle } from '../../utils/themes/theme'; -import { buildBarRenderProps, buildBarBorderRenderProps } from './utils/rendering_props_utils'; +import { buildBarRenderProps, buildBarBorderRenderProps, Clippings } from './utils/rendering_props_utils'; interface BarGeometriesDataProps { animated?: boolean; bars: BarGeometry[]; sharedStyle: SharedGeometryStateStyle; highlightedLegendItem: LegendItem | null; - clippings: ContainerConfig; + clippings: Clippings; } interface BarGeometriesDataState { overBar?: BarGeometry; diff --git a/src/components/react_canvas/line_geometries.tsx b/src/components/react_canvas/line_geometries.tsx index 4ee5183b89..20799e5207 100644 --- a/src/components/react_canvas/line_geometries.tsx +++ b/src/components/react_canvas/line_geometries.tsx @@ -1,4 +1,4 @@ -import { Group as KonvaGroup, ContainerConfig } from 'konva'; +import { Group as KonvaGroup } from 'konva'; import React from 'react'; import { Circle, Group, Path } from 'react-konva'; import { LegendItem } from '../../chart_types/xy_chart/legend/legend'; @@ -14,6 +14,8 @@ import { buildPointStyleProps, PointStyleProps, buildPointRenderProps, + Clippings, + clipRanges, } from './utils/rendering_props_utils'; import { mergePartial } from '../../utils/commons'; @@ -22,7 +24,7 @@ interface LineGeometriesDataProps { lines: LineGeometry[]; sharedStyle: SharedGeometryStateStyle; highlightedLegendItem: LegendItem | null; - clippings: ContainerConfig; + clippings: Clippings; } interface LineGeometriesDataState { overPoint?: PointGeometry; @@ -92,9 +94,23 @@ export class LineGeometries extends React.PureComponent 0) { + return ( + + + + + + + + + ); + } + return ( diff --git a/src/components/react_canvas/reactive_chart.tsx b/src/components/react_canvas/reactive_chart.tsx index 988f109034..abe277b18e 100644 --- a/src/components/react_canvas/reactive_chart.tsx +++ b/src/components/react_canvas/reactive_chart.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { inject, observer } from 'mobx-react'; -import { ContainerConfig } from 'konva'; import { Layer, Rect, Stage } from 'react-konva'; + import { AnnotationId } from '../../utils/ids'; import { isLineAnnotation, isRectAnnotation, AxisSpec } from '../../chart_types/xy_chart/utils/specs'; import { LineAnnotationStyle, RectAnnotationStyle, mergeGridLineConfigs } from '../../utils/themes/theme'; @@ -22,6 +22,7 @@ import { LineGeometries } from './line_geometries'; import { RectAnnotation } from './rect_annotation'; import { AxisTick, AxisTicksDimensions, isVerticalGrid } from '../../chart_types/xy_chart/utils/axis_utils'; import { Dimensions } from '../../utils/dimensions'; +import { Clippings } from './utils/rendering_props_utils'; interface ReactiveChartProps { chartStore?: ChartStore; // FIX until we find a better way on ts mobx @@ -88,7 +89,7 @@ class Chart extends React.Component { window.removeEventListener('mouseup', this.onEndBrushing); } - renderBarSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => { + renderBarSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!; if (!geometries) { return []; @@ -113,7 +114,7 @@ class Chart extends React.Component { }, ]; }; - renderLineSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => { + renderLineSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!; if (!geometries) { return []; @@ -139,7 +140,7 @@ class Chart extends React.Component { }, ]; }; - renderAreaSeries = (clippings: ContainerConfig): ReactiveChartElementIndex[] => { + renderAreaSeries = (clippings: Clippings): ReactiveChartElementIndex[] => { const { geometries, canDataBeAnimated, chartTheme } = this.props.chartStore!; if (!geometries) { return []; @@ -352,11 +353,12 @@ class Chart extends React.Component { sortAndRenderElements() { const { chartRotation, chartDimensions } = this.props.chartStore!; + const { height, width } = chartDimensions; const clippings = { clipX: 0, clipY: 0, - clipWidth: [90, -90].includes(chartRotation) ? chartDimensions.height : chartDimensions.width, - clipHeight: [90, -90].includes(chartRotation) ? chartDimensions.width : chartDimensions.height, + clipWidth: [90, -90].includes(chartRotation) ? height : width, + clipHeight: [90, -90].includes(chartRotation) ? width : height, }; const bars = this.renderBarSeries(clippings); diff --git a/src/components/react_canvas/utils/rendering_props_utils.test.ts b/src/components/react_canvas/utils/rendering_props_utils.test.ts index e5fe6e8a06..581fab3cd7 100644 --- a/src/components/react_canvas/utils/rendering_props_utils.test.ts +++ b/src/components/react_canvas/utils/rendering_props_utils.test.ts @@ -10,8 +10,12 @@ import { rotateBarValueProps, buildPointStyleProps, buildBarBorderRenderProps, + Clippings, + clipRanges, } from './rendering_props_utils'; import { RectBorderStyle, RectStyle } from '../../../utils/themes/theme'; +import { forcedType } from '../../../mocks/utils'; +import { ClippedRanges } from '../../../chart_types/xy_chart/rendering/rendering'; describe('[canvas] Area Geometries props', () => { test('can build area point props', () => { @@ -977,4 +981,100 @@ describe('[canvas] Bar Geometries', () => { expect(props).toBeNull(); }); }); + + describe('clipRanges', () => { + const clippedRanges: ClippedRanges = [[0, 1], [2, 4], [4, 6], [7, 11], [11, 12]]; + const singleRange: ClippedRanges = [[0, 1]]; + const clippings: Clippings = { + clipHeight: 111, + clipWidth: 222, + }; + const mockCtx = forcedType({ + rect: jest.fn(), + }); + + describe('clipping is NOT negated', () => { + it('should call ctx with correct args - empty range', () => { + clipRanges([], clippings, false)(mockCtx); + + expect(mockCtx.rect).not.toBeCalled(); + }); + + describe('length equal to 1', () => { + it('should call ctx with correct args for start range - single range', () => { + clipRanges(singleRange, clippings, false)(mockCtx); + + expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, singleRange[0][0], clippings.clipHeight); + }); + + it('should call ctx with correct args for end range - single range', () => { + clipRanges(singleRange, clippings, false)(mockCtx); + const lastX = singleRange[singleRange.length - 1][1]; + + expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight); + }); + + it('should only call ctx twice', () => { + clipRanges(singleRange, clippings, false)(mockCtx); + + expect(mockCtx.rect).toBeCalledTimes(2); + }); + }); + + describe('length greater than 1', () => { + it('should call ctx with correct args for start range - single range', () => { + clipRanges(clippedRanges, clippings, false)(mockCtx); + + expect(mockCtx.rect).toHaveBeenNthCalledWith(1, 0, 0, clippedRanges[0][0], clippings.clipHeight); + }); + + it('should call ctx with correct args for end range - single range', () => { + clipRanges(clippedRanges, clippings, false)(mockCtx); + const lastX = clippedRanges[clippedRanges.length - 1][1]; + + expect(mockCtx.rect).toHaveBeenNthCalledWith(2, lastX, 0, clippings.clipWidth - lastX, clippings.clipHeight); + }); + + it('should call ctx with correct args', () => { + clipRanges(clippedRanges, clippings, false)(mockCtx); + + for (let i = 1; i < length; i++) { + const [, x0] = clippedRanges[i - 1]; + const [x1] = clippedRanges[i]; + + expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 3, x0, 0, x1 - x0, clippings.clipHeight); + } + }); + + it('should only call ctx for (n - 1) range plus 2 for ends', () => { + clipRanges(clippedRanges, clippings, false)(mockCtx); + + expect(mockCtx.rect).toBeCalledTimes(clippedRanges.length - 1 + 2); + }); + }); + }); + + describe('clipping is negated', () => { + it('should call ctx with correct args - empty range', () => { + clipRanges([], clippings, true)(mockCtx); + + expect(mockCtx.rect).not.toBeCalled(); + }); + + it('should call ctx with correct args - single range', () => { + clipRanges(singleRange, clippings, true)(mockCtx); + const [x0, x1] = clippedRanges[0]; + + expect(mockCtx.rect).toHaveBeenNthCalledWith(1, x0, 0, x1 - x0, clippings.clipHeight); + }); + + it('should call ctx with correct args', () => { + clipRanges(clippedRanges, clippings, true)(mockCtx); + + clippedRanges.forEach(([x0, x1], i) => { + expect(mockCtx.rect).toHaveBeenNthCalledWith(i + 1, x0, 0, x1 - x0, clippings.clipHeight); + }); + }); + }); + }); }); diff --git a/src/components/react_canvas/utils/rendering_props_utils.ts b/src/components/react_canvas/utils/rendering_props_utils.ts index d6994218c3..0c320da9d2 100644 --- a/src/components/react_canvas/utils/rendering_props_utils.ts +++ b/src/components/react_canvas/utils/rendering_props_utils.ts @@ -1,3 +1,7 @@ +import { RectConfig, PathConfig, CircleConfig, ContainerConfig } from 'konva'; +import { Required } from 'utility-types'; + +import { ClippedRanges } from '../../../chart_types/xy_chart/rendering/rendering'; import { Rotation } from '../../../chart_types/xy_chart/utils/specs'; import { AreaStyle, @@ -10,7 +14,6 @@ import { } from '../../../utils/themes/theme'; import { Dimensions } from '../../../utils/dimensions'; import { GlobalKonvaElementProps } from '../globals'; -import { RectConfig, PathConfig, CircleConfig } from 'konva'; export interface PointStyleProps { radius: number; @@ -21,6 +24,8 @@ export interface PointStyleProps { opacity: number; } +export type Clippings = Required; + export function rotateBarValueProps( chartRotation: Rotation, chartDimensions: Dimensions, @@ -416,3 +421,44 @@ export function buildBarBorderRenderProps( ...GlobalKonvaElementProps, }; } + +/** + * Creates `clipFunc` for Konva paths that have clipped ranges + * + * @param clippedRanges ranges to be clipped from rendering + * @param clippings konva global clippings + * @param negate show, rather than exclude, only selected ranges + */ +export function clipRanges( + clippedRanges: ClippedRanges, + clippings: Clippings, + negate = false, +): (ctx: CanvasRenderingContext2D) => void { + const length = clippedRanges.length; + const { clipHeight, clipWidth } = clippings; + + if (negate) { + return (ctx) => { + clippedRanges.forEach(([x0, x1]) => { + ctx.rect(x0, 0, x1 - x0, clippings.clipHeight); + }); + }; + } + + return (ctx) => { + if (length > 0) { + ctx.rect(0, 0, clippedRanges[0][0], clipHeight); + const lastX = clippedRanges[length - 1][1]; + ctx.rect(lastX, 0, clipWidth - lastX, clipHeight); + } + + if (length > 1) { + for (let i = 1; i < length; i++) { + const [, x0] = clippedRanges[i - 1]; + const [x1] = clippedRanges[i]; + + ctx.rect(x0, 0, x1 - x0, clipHeight); + } + } + }; +} diff --git a/src/mocks/index.ts b/src/mocks/index.ts new file mode 100644 index 0000000000..0ca60a9092 --- /dev/null +++ b/src/mocks/index.ts @@ -0,0 +1 @@ +export * from './series'; diff --git a/src/mocks/models.ts b/src/mocks/models.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/mocks/scale/index.ts b/src/mocks/scale/index.ts new file mode 100644 index 0000000000..44544b730a --- /dev/null +++ b/src/mocks/scale/index.ts @@ -0,0 +1 @@ +export * from './scale'; diff --git a/src/mocks/scale/scale.ts b/src/mocks/scale/scale.ts new file mode 100644 index 0000000000..53384e29f1 --- /dev/null +++ b/src/mocks/scale/scale.ts @@ -0,0 +1,25 @@ +import { mergePartial } from '../../utils/commons'; +import { Scale, ScaleType } from '../../utils/scales/scales'; + +export class MockScale { + private static readonly base: Scale = { + scale: jest.fn().mockImplementation((x) => x), + type: ScaleType.Linear, + bandwidth: 0, + minInterval: 0, + barsPadding: 0, + range: [0, 100], + domain: [0, 100], + ticks: jest.fn(), + pureScale: jest.fn(), + invert: jest.fn(), + invertWithStep: jest.fn(), + isSingleValue: jest.fn(), + isValueInDomain: jest.fn(), + isInverted: false, + }; + + static default(partial: Partial): Scale { + return mergePartial(MockScale.base, partial); + } +} diff --git a/src/mocks/series/data.ts b/src/mocks/series/data.ts new file mode 100644 index 0000000000..2718eb98e8 --- /dev/null +++ b/src/mocks/series/data.ts @@ -0,0 +1,147 @@ +import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series'; + +export const fitFunctionData: DataSeriesDatum[] = [ + { + x: 0, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 0, + y: null, + }, + }, + { + x: 1, + y1: 3, + y0: 0, + initialY1: 3, + initialY0: null, + datum: { + x: 1, + y: 3, + }, + }, + { + x: 2, + y1: 5, + y0: 0, + initialY1: 5, + initialY0: null, + datum: { + x: 2, + y: 5, + }, + }, + { + x: 3, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 3, + y: null, + }, + }, + { + x: 4, + y1: 4, + y0: 0, + initialY1: 4, + initialY0: null, + datum: { + x: 4, + y: 4, + }, + }, + { + x: 5, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 5, + y: null, + }, + }, + { + x: 6, + y1: 5, + y0: 0, + initialY1: 5, + initialY0: null, + datum: { + x: 6, + y: 5, + }, + }, + { + x: 7, + y1: 6, + y0: 0, + initialY1: 6, + initialY0: null, + datum: { + x: 7, + y: 6, + }, + }, + { + x: 8, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 8, + y: null, + }, + }, + { + x: 9, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 9, + y: null, + }, + }, + { + x: 10, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 10, + y: null, + }, + }, + { + x: 11, + y1: 12, + y0: 0, + initialY1: 12, + initialY0: null, + datum: { + x: 11, + y: 12, + }, + }, + { + x: 12, + y1: null, + y0: null, + initialY1: null, + initialY0: null, + datum: { + x: 12, + y: null, + }, + }, +]; diff --git a/src/mocks/series/index.ts b/src/mocks/series/index.ts new file mode 100644 index 0000000000..48a723d4ab --- /dev/null +++ b/src/mocks/series/index.ts @@ -0,0 +1,2 @@ +export * from './series'; +export * from './utils'; diff --git a/src/mocks/series/series.ts b/src/mocks/series/series.ts new file mode 100644 index 0000000000..d8ba605dc8 --- /dev/null +++ b/src/mocks/series/series.ts @@ -0,0 +1,181 @@ +import { shuffle } from 'lodash'; + +import { mergePartial } from '../../utils/commons'; +import { getSpecId } from '../..'; +import { + DataSeries, + DataSeriesDatum, + RawDataSeries, + RawDataSeriesDatum, +} from '../../chart_types/xy_chart/utils/series'; +import { fitFunctionData } from './data'; +import { FullDataSeriesDatum, WithIndex } from '../../chart_types/xy_chart/utils/fit_function'; + +export class MockDataSeries { + private static readonly base: DataSeries = { + specId: getSpecId('spec1'), + key: ['spec1'], + seriesColorKey: 'spec1', + data: [], + }; + + static default(partial?: Partial) { + return mergePartial(MockDataSeries.base, partial, { mergeOptionalPartialValues: true }); + } + + static fitFunction( + options: { shuffle?: boolean; ordinal?: boolean } = { shuffle: true, ordinal: false }, + ): DataSeries { + const ordinalData = options.ordinal + ? fitFunctionData.map((d) => ({ ...d, x: String.fromCharCode(97 + (d.x as number)) })) + : fitFunctionData; + const data = options.shuffle && !options.ordinal ? shuffle(ordinalData) : ordinalData; + + return { + ...MockDataSeries.base, + data, + }; + } + + static withData(data: DataSeries['data']): DataSeries { + return { + ...MockDataSeries.base, + data, + }; + } +} + +export class MockRawDataSeries { + private static readonly base: RawDataSeries = { + specId: getSpecId('spec1'), + key: ['spec1'], + seriesColorKey: 'spec1', + data: [], + }; + + static default(partial?: Partial) { + return mergePartial(MockRawDataSeries.base, partial); + } + + static fitFunction( + options: { shuffle?: boolean; ordinal?: boolean } = { shuffle: true, ordinal: false }, + ): RawDataSeries { + const rawData = fitFunctionData.map(({ initialY0, initialY1, filled, ...datum }) => datum); + const ordinalData = options.ordinal + ? rawData.map((d) => ({ ...d, x: String.fromCharCode(97 + (d.x as number)) })) + : rawData; + const data = options.shuffle && !options.ordinal ? shuffle(ordinalData) : ordinalData; + + return { + ...MockRawDataSeries.base, + data, + }; + } + + static withData(data: RawDataSeries['data']): RawDataSeries { + return { + ...MockRawDataSeries.base, + data, + }; + } +} + +export class MockDataSeriesDatum { + private static readonly base: DataSeriesDatum = { + x: 1, + y1: 1, + y0: 1, + initialY1: 1, + initialY0: 1, + datum: {}, + }; + + static default(partial?: Partial): DataSeriesDatum { + return mergePartial(MockDataSeriesDatum.base, partial, { mergeOptionalPartialValues: true }); + } + + /** + * Fill datum with minimal values, default missing required values to `null` + */ + static simple({ + x, + y1 = null, + y0 = null, + filled, + }: Partial & Pick): DataSeriesDatum { + return { + x, + y1, + y0, + initialY1: y1, + initialY0: y0, + ...(filled && filled), + }; + } + + /** + * returns "full" datum with minimal values, default missing required values to `null` + * + * "full" - means x and y1 values are `non-nullable` + */ + static full({ + fittingIndex = 0, + ...datum + }: Partial> & Pick, 'x' | 'y1'>): WithIndex< + FullDataSeriesDatum + > { + return { + ...(MockDataSeriesDatum.simple(datum) as WithIndex), + fittingIndex, + }; + } + + static ordinal(partial?: Partial): DataSeriesDatum { + return mergePartial( + { + ...MockDataSeriesDatum.base, + x: 'a', + }, + partial, + { mergeOptionalPartialValues: true }, + ); + } +} + +export class MockRawDataSeriesDatum { + private static readonly base: RawDataSeriesDatum = { + x: 1, + y1: 1, + y0: 1, + datum: {}, + }; + + static default(partial?: Partial): RawDataSeriesDatum { + return mergePartial(MockRawDataSeriesDatum.base, partial); + } + + /** + * Fill raw datum with minimal values, default missing required values to `null` + */ + static simple({ + x, + y1 = null, + y0 = null, + }: Partial & Pick): RawDataSeriesDatum { + return { + x, + y1, + y0, + }; + } + + static ordinal(partial?: Partial): RawDataSeriesDatum { + return mergePartial( + { + ...MockRawDataSeriesDatum.base, + x: 'a', + }, + partial, + ); + } +} diff --git a/src/mocks/series/utils.ts b/src/mocks/series/utils.ts new file mode 100644 index 0000000000..ef99a18f59 --- /dev/null +++ b/src/mocks/series/utils.ts @@ -0,0 +1,30 @@ +import { DataSeriesDatum } from '../../chart_types/xy_chart/utils/series'; +import { getYValue } from '../../chart_types/xy_chart/rendering/rendering'; + +/** + * Helper function to return array of rendered y1 values + */ +export const getFilledNullData = (data: DataSeriesDatum[]): (number | undefined)[] => { + return data.filter(({ y1 }) => y1 === null).map(({ filled }) => filled && filled.y1); +}; + +/** + * Helper function to return array of rendered y1 values + */ +export const getFilledNonNullData = (data: DataSeriesDatum[]): (number | undefined)[] => { + return data.filter(({ y1 }) => y1 !== null).map(({ filled }) => filled && filled.y1); +}; + +/** + * Helper function to return array of rendered x values + */ +export const getXValueData = (data: DataSeriesDatum[]): (number | string)[] => { + return data.map(({ x }) => x); +}; + +/** + * Returns value of `y1` or `filled.y1` or null + */ +export const getYResolvedData = (data: DataSeriesDatum[]): (number | null)[] => { + return data.map(getYValue); +}; diff --git a/src/mocks/specs/index.ts b/src/mocks/specs/index.ts new file mode 100644 index 0000000000..60a182ae95 --- /dev/null +++ b/src/mocks/specs/index.ts @@ -0,0 +1 @@ +export * from './specs'; diff --git a/src/mocks/specs/specs.ts b/src/mocks/specs/specs.ts new file mode 100644 index 0000000000..c889979131 --- /dev/null +++ b/src/mocks/specs/specs.ts @@ -0,0 +1,101 @@ +import { mergePartial } from '../../utils/commons'; +import { + SeriesSpecs, + DEFAULT_GLOBAL_ID, + BarSeriesSpec, + AreaSeriesSpec, + HistogramModeAlignments, + HistogramBarSeriesSpec, + LineSeriesSpec, + BasicSeriesSpec, +} from '../../chart_types/xy_chart/utils/specs'; +import { getSpecId, getGroupId, SpecId } from '../../utils/ids'; +import { ScaleType } from '../../utils/scales/scales'; + +export class MockSeriesSpec { + private static readonly barBase: BarSeriesSpec = { + id: getSpecId('spec1'), + seriesType: 'bar', + groupId: getGroupId(DEFAULT_GLOBAL_ID), + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + yScaleToDataExtent: false, + hideInLegend: false, + enableHistogramMode: false, + stackAsPercentage: false, + data: [], + }; + + private static readonly histogramBarBase: HistogramBarSeriesSpec = { + id: getSpecId('spec1'), + seriesType: 'bar', + groupId: getGroupId(DEFAULT_GLOBAL_ID), + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + yScaleToDataExtent: false, + hideInLegend: false, + enableHistogramMode: true, + data: [], + }; + + private static readonly areaBase: AreaSeriesSpec = { + id: getSpecId('spec1'), + seriesType: 'area', + groupId: getGroupId(DEFAULT_GLOBAL_ID), + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + yScaleToDataExtent: false, + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, + data: [], + }; + + private static readonly lineBase: LineSeriesSpec = { + id: getSpecId('spec1'), + seriesType: 'line', + groupId: getGroupId(DEFAULT_GLOBAL_ID), + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 'x', + yAccessors: ['y'], + yScaleToDataExtent: false, + hideInLegend: false, + histogramModeAlignment: HistogramModeAlignments.Center, + data: [], + }; + + static bar(partial?: Partial): BarSeriesSpec { + return mergePartial(MockSeriesSpec.barBase, partial, { mergeOptionalPartialValues: true }); + } + + static histogramBar(partial?: Partial): HistogramBarSeriesSpec { + return mergePartial(MockSeriesSpec.histogramBarBase, partial, { + mergeOptionalPartialValues: true, + }); + } + + static area(partial?: Partial): AreaSeriesSpec { + return mergePartial(MockSeriesSpec.areaBase, partial, { mergeOptionalPartialValues: true }); + } + + static line(partial?: Partial): LineSeriesSpec { + return mergePartial(MockSeriesSpec.lineBase, partial, { mergeOptionalPartialValues: true }); + } +} + +export class MockSeriesSpecs { + static fromSpecs(specs: BasicSeriesSpec[]): SeriesSpecs { + const specsMap: [SpecId, BasicSeriesSpec][] = specs.map((spec) => [spec.id, spec]); + return new Map(specsMap); + } + + static empty(): SeriesSpecs { + return new Map(); + } +} diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts new file mode 100644 index 0000000000..a7bd61e59a --- /dev/null +++ b/src/mocks/utils.ts @@ -0,0 +1,10 @@ +/** + * Forces object to be partial type for mocking tests + * + * SHOULD NOT BE USED OUTSIDE OF TESTS!!! + * + * @param obj partial object type + */ +export const forcedType = (obj: Partial): T => { + return obj as T; +}; diff --git a/src/utils/commons.ts b/src/utils/commons.ts index 84201a1296..f928d8d72b 100644 --- a/src/utils/commons.ts +++ b/src/utils/commons.ts @@ -12,9 +12,6 @@ export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -// Can remove once we upgrade to TypesScript >= 3.5 -export type Omit = Pick>; - /** * This function returns a function to generate ids. * This can be used to generate unique, but predictable ids to pair labels diff --git a/src/utils/curves.ts b/src/utils/curves.ts index 426fe703c5..662f470ced 100644 --- a/src/utils/curves.ts +++ b/src/utils/curves.ts @@ -10,6 +10,7 @@ import { curveStepAfter, curveStepBefore, } from 'd3-shape'; +import { $Values } from 'utility-types'; export const CurveType = Object.freeze({ CURVE_CARDINAL: 0 as 0, @@ -24,17 +25,7 @@ export const CurveType = Object.freeze({ LINEAR: 9 as 9, }); -export type CurveType = - | typeof CurveType.CURVE_CARDINAL - | typeof CurveType.CURVE_NATURAL - | typeof CurveType.CURVE_MONOTONE_X - | typeof CurveType.CURVE_MONOTONE_Y - | typeof CurveType.CURVE_BASIS - | typeof CurveType.CURVE_CATMULL_ROM - | typeof CurveType.CURVE_STEP - | typeof CurveType.CURVE_STEP_AFTER - | typeof CurveType.CURVE_STEP_BEFORE - | typeof CurveType.LINEAR; +export type CurveType = $Values; export function getCurveFactory(curveType: CurveType = CurveType.LINEAR) { switch (curveType) { diff --git a/src/utils/scales/scales.ts b/src/utils/scales/scales.ts index 0a7ccd3119..587906b10e 100644 --- a/src/utils/scales/scales.ts +++ b/src/utils/scales/scales.ts @@ -1,3 +1,5 @@ +import { $Values } from 'utility-types'; + export interface Scale { domain: any[]; range: number[]; @@ -39,12 +41,7 @@ export const ScaleType = Object.freeze({ Time: 'time' as 'time', }); -export type ScaleType = - | typeof ScaleType.Linear - | typeof ScaleType.Sqrt - | typeof ScaleType.Log - | typeof ScaleType.Time - | typeof ScaleType.Ordinal; +export type ScaleType = $Values; export interface ScaleConfig { accessor: (value: any) => any; diff --git a/stories/mixed.tsx b/stories/mixed.tsx index a152c9ba13..47619d757b 100644 --- a/stories/mixed.tsx +++ b/stories/mixed.tsx @@ -1,6 +1,8 @@ import { storiesOf } from '@storybook/react'; +import { select, number } from '@storybook/addon-knobs'; import { DateTime } from 'luxon'; import React from 'react'; + import { AreaSeries, Axis, @@ -15,6 +17,7 @@ import { Settings, } from '../src/'; import { timeFormatter } from '../src/utils/data/formatters'; +import { Fit } from '../src/chart_types/xy_chart/utils/specs'; storiesOf('Mixed Charts', module) .add('bar and lines', () => { @@ -217,4 +220,182 @@ storiesOf('Mixed Charts', module) /> ); + }) + .add('Fitting functions - non-stacked series', () => { + const dataTypes = { + isolated: [ + { x: 0, y: 3 }, + { x: 1, y: 5 }, + { x: 2, y: null }, + { x: 3, y: 4 }, + { x: 4, y: null }, + { x: 5, y: 5 }, + { x: 6, y: null }, + { x: 7, y: 12 }, + { x: 8, y: null }, + { x: 9, y: 10 }, + { x: 10, y: 7 }, + ], + successive: [ + { x: 0, y: 3 }, + { x: 1, y: 5 }, + { x: 2, y: null }, + { x: 4, y: null }, + { x: 6, y: null }, + { x: 8, y: null }, + { x: 9, y: 10 }, + { x: 10, y: 7 }, + ], + endPoints: [ + { x: 0, y: null }, + { x: 1, y: 5 }, + { x: 3, y: 4 }, + { x: 5, y: 5 }, + { x: 7, y: 12 }, + { x: 9, y: 10 }, + { x: 10, y: null }, + ], + ordinal: [ + { x: 'a', y: null }, + { x: 'b', y: 3 }, + { x: 'c', y: 5 }, + { x: 'd', y: null }, + { x: 'e', y: 4 }, + { x: 'f', y: null }, + { x: 'g', y: 5 }, + { x: 'h', y: 6 }, + { x: 'i', y: null }, + { x: 'j', y: null }, + { x: 'k', y: null }, + { x: 'l', y: 12 }, + { x: 'm', y: null }, + ], + all: [ + { x: 0, y: null }, + { x: 1, y: 3 }, + { x: 2, y: 5 }, + { x: 3, y: null }, + { x: 4, y: 4 }, + { x: 5, y: null }, + { x: 6, y: 5 }, + { x: 7, y: 6 }, + { x: 8, y: null }, + { x: 9, y: null }, + { x: 10, y: null }, + { x: 11, y: 12 }, + { x: 12, y: null }, + ], + }; + + const seriesType = select( + 'seriesType', + { + Area: 'area', + Line: 'line', + }, + 'area', + ); + const dataKey = select( + 'dataset', + { + 'Isolated Points': 'isolated', + 'Successive null Points': 'successive', + 'null end points': 'endPoints', + 'Ordinal x values': 'ordinal', + 'All edge cases': 'all', + }, + 'all', + ); + // @ts-ignore + const dataset = dataTypes[dataKey]; + const fit = select( + 'fitting function', + { + None: Fit.None, + Carry: Fit.Carry, + Lookahead: Fit.Lookahead, + Nearest: Fit.Nearest, + Average: Fit.Average, + Linear: Fit.Linear, + Zero: Fit.Zero, + Explicit: Fit.Explicit, + }, + Fit.Average, + ); + const curve = select( + 'Curve', + { + 'Curve cardinal': CurveType.CURVE_CARDINAL, + 'Curve natural': CurveType.CURVE_NATURAL, + 'Curve monotone x': CurveType.CURVE_MONOTONE_X, + 'Curve monotone y': CurveType.CURVE_MONOTONE_Y, + 'Curve basis': CurveType.CURVE_BASIS, + 'Curve catmull rom': CurveType.CURVE_CATMULL_ROM, + 'Curve step': CurveType.CURVE_STEP, + 'Curve step after': CurveType.CURVE_STEP_AFTER, + 'Curve step before': CurveType.CURVE_STEP_BEFORE, + Linear: CurveType.LINEAR, + }, + 0, + ); + const endValue = select( + 'End value', + { + None: 'none', + nearest: 'nearest', + '0': 0, + '2': 2, + }, + 'none', + ); + const parsedEndValue: number | 'nearest' = Number.isNaN(Number(endValue)) ? 'nearest' : Number(endValue); + const value = number('Explicit valuve (using Fit.Explicit)', 5); + const xScaleType = dataKey === 'ordinal' ? ScaleType.Ordinal : ScaleType.Linear; + + return ( + + + + + {seriesType === 'area' ? ( + + ) : ( + + )} + + ); }); diff --git a/tsconfig.jest.json b/tsconfig.jest.json index 5cd2e875f6..f407711d1d 100644 --- a/tsconfig.jest.json +++ b/tsconfig.jest.json @@ -1,6 +1,8 @@ { - "extends": "./tsconfig", + "extends": "./tsconfig.json", "include": [ - "src/**/*" + "src/**/*", + "scripts/setup_enzyme.ts", + "scripts/custom_matchers.ts" ] } diff --git a/tsconfig.json b/tsconfig.json index 9e69b0d363..78daac7661 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,7 @@ "jsx": "react", "allowJs": false, "skipLibCheck": true, - "downlevelIteration": true + "downlevelIteration": true, + "typeRoots": ["./node_modules/@types/", "./global.d.ts"] } } diff --git a/wiki/consuming.md b/wiki/consuming.md index 013ec2da6e..83813ccc03 100644 --- a/wiki/consuming.md +++ b/wiki/consuming.md @@ -15,6 +15,7 @@ To use Elastic Charts code in Kibana, check if `@elastic/charts` packages is alr ## Using Elastic Charts in a standalone project You can consume Elastic Charts in standalone projects, such as plugins and prototypes. Elastic-Charts has a peer dependency on [moment-timezone](https://momentjs.com/timezone/). Add that dependency on your main project with: + ``` yarn add moment-timezone ``` @@ -63,6 +64,6 @@ Use a `.babelrc` config with the [`usebuiltins`](https://babeljs.io/docs/en/babe Directly import polyfill and runtime. ```js -import "core-js/stable"; -import "regenerator-runtime/runtime"; +import 'core-js/stable'; +import 'regenerator-runtime/runtime'; ``` diff --git a/yarn.lock b/yarn.lock index bfa3dd42e5..596bce006d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6780,7 +6780,7 @@ expect-puppeteer@^4.3.0: resolved "https://registry.yarnpkg.com/expect-puppeteer/-/expect-puppeteer-4.3.0.tgz#732a3c94ab44af0c7d947040ad3e3637a0359bf3" integrity sha512-p8N/KSVPG9PAOJlftK5f1n3JrULJ6Qq1EQ8r/n9xzkX2NmXbK8PcnJnkSAEzEHrMycELKGnlJV7M5nkgm+wEWA== -expect@^24.9.0: +expect@^24.1.0, expect@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== @@ -9122,6 +9122,20 @@ jest-environment-puppeteer@^4.3.0: jest-dev-server "^4.3.0" merge-deep "^3.0.2" +jest-extended@^0.11.2: + version "0.11.2" + resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-0.11.2.tgz#924f4a6b4c946133faf9ec8fba865de9790f4116" + integrity sha512-gwNMXrAPN0IY5L7VXWfSlC2aGo0KHIsGGcW+lTHYpedt5SJksEvBgMxs29iNikiNOz+cqAZY1s/+kYK0jlj4Jw== + dependencies: + expect "^24.1.0" + jest-get-type "^22.4.3" + jest-matcher-utils "^22.0.0" + +jest-get-type@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" + integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== + jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" @@ -9190,6 +9204,15 @@ jest-leak-detector@^24.9.0: jest-get-type "^24.9.0" pretty-format "^24.9.0" +jest-matcher-utils@^22.0.0: + version "22.4.3" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff" + integrity sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA== + dependencies: + chalk "^2.0.1" + jest-get-type "^22.4.3" + pretty-format "^22.4.3" + jest-matcher-utils@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" @@ -10135,7 +10158,7 @@ lodash@4.17.14: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba" integrity sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw== -lodash@4.17.15, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10: +lodash@4.17.15, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.2.1, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -12389,6 +12412,14 @@ pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" +pretty-format@^22.4.3: + version "22.4.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" + integrity sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ== + dependencies: + ansi-regex "^3.0.0" + ansi-styles "^3.2.0" + pretty-format@^24.9.0: version "24.9.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" @@ -15855,6 +15886,11 @@ utila@^0.4.0, utila@~0.4: resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= +utility-types@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.8.0.tgz#eaae1c2520c206b5623a4f07af38e3aa31dc2f06" + integrity sha512-UoKivAmVw5TL6AHuILieOJjIK9ajS0l5gyN5LbJglPuVwzfYBniDhe+3A5+ZBtS0TAHQs5qUxTwj9jXurINrcw== + utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"